7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Render Propsパターンを使って、再帰ツリーUIの再利用性を高める

7
Last updated at Posted at 2025-12-15

この記事は株式会社カオナビ Advent Calendar 2025の16日目です。

はじめに

私はReactでツリー構造のUIを作成しています。
このUIはいずれ複数の箇所で利用する可能性がありそうなので、再利用しやすい状態にしたいと考えています。

この記事では、開発中のツリー構造UIの再利用性をあげるために使用した「Render Propsパターン」について紹介します。

作っているUIのイメージ

下記のようなイメージでツリー構造のUIを開発しています。

スクリーンショット 2025-12-15 8.51.27.png

ツリー構造UIコンポーネントのReact実装イメージ

以下のように、再帰的にツリー構造UIを構築しています。

type TreeNode = {
  id: number;
  name: string;
  children: TreeNode[]; // 再帰的な構造
};

type TreeViewProps = {
  nodes: TreeNode[];
};

const TreeView: React.FC<TreeViewProps> = ({ nodes }) => {
  return (
    <ul>
      {nodes.map((node) => (
        <TreeBranch key={node.id} node={node} />
      ))}
    </ul>
  );
};

type TreeBranchProps = {
  node: TreeNode;
};

const TreeBranch: React.FC<TreeBranchProps> = ({ node }) => {
  const hasChildren = node.children.length > 0;

  return (
    <li>
      {/* ノード本体 */}
      <p>{node.name}</p>

      {/* 子ノードの再帰的なレンダリング */}
      {hasChildren && (
        <ul>
          {node.children.map((child) => (
            <TreeBranch key={child.id} node={child} />
          ))}
        </ul>
      )}
    </li>
  );
};

発生した問題

各ノードの部分に追加でAPIから得た詳細情報を表示させたいということになりました。
スクリーンショット 2025-12-15 8.54.50.png

const TreeBranch: React.FC<{ node: TreeNode }> = ({ node }) => {
  const hasChildren = node.children.length > 0;

  return (
    <li>
      {/* ノード本体 */}
      <div>
          <p>{node.name}</p>
          
          {/* ⚠️ここに追加でAPIから得たデータを表示したい ⚠️*/}
          {/* 例: <NodeDetail nodeId={node.id} /> */}
      </div>

      {/* 子ノードの再帰的なレンダリング */}
      {hasChildren && (
        <ul>
          {node.children.map((child) => (
            <TreeBranch key={child.id} node={child} />
          ))}
        </ul>
      )}
    </li>
  );
};

素直にノードの内容を実装すると下記のような実装になります。

const TreeBranch: React.FC<{ node: TreeNode }> = ({ node }) => {
  const hasChildren = node.children.length > 0;

  // ノードの詳細をgetするAPIなどが内包されている
  const{ isLoading, data } = useNodeDetail(node.id);

  return (
    <li>
      {/* ノード本体 */}
      <div>
          <p>{node.name}</p>
          
          {/* ノードの説明をAPIから取得して表示する */}
          {isLoading && <p>…loading</p>}
          {!isLoading && <p>{data.description}</p>}
      </div>

      {/* 子ノードの再帰的なレンダリング */}
      {hasChildren && (
        <ul>
          {node.children.map((child) => (
            <TreeBranch key={child.id} node={child} />
          ))}
        </ul>
      )}
    </li>
  );
};

しかし、これには以下のような問題があります。

  • 叩くAPIはTreeViewの利用箇所で変わるが、それに対応できない
    • 取得タイミングなども利用箇所で変わる(初期描画時なのか、展開時なのか、そもそも叩かない場合もあるかも)が、全てに対応しようとすると複雑になる
  • 各ノードのUIをTreeViewの利用箇所ごとにカスタムできない

対策として「APIリクエストの関数や表示方法に関する情報をPropsで受け取り、TreeBranch内で呼ぶ」案も検討しました。
しかしこの場合も、TreeView内で、その関数をいつ・どの条件で呼ぶかの判断を握っている状態となります。
ツリーは「APIを呼ぶタイミング」といった取得のロジックやUIも握ったままなので、それを利用側の各コンポーネントの都合に合わせようとして、どんどん分岐が増えていく未来が見えます。

こうなると、TreeViewが「APIを叩く」や「内容をどう表示する」かなどの責務をどんどん抱え込んでしまい、再利用性や保守性が低下していきます。

私はこのような状態を避け、もっとTreeViewコンポーネントの責務を「再帰的なツリーUIの描画」だけに集中させられないか?と考えました。

Render Propsパターンを使った解決策

Render Propsパターンとは

詳細な説明はPatterns.devをご参照ください。
https://www.patterns.dev/react/render-props-pattern/

一言で表現すると以下の通りです(AIによる要約)。

コンポーネントが、実際の描画内容(JSX要素)を返す関数をプロパティとして受け取り、その関数を呼び出すことでコンポーネントのロジックやデータの共有と再利用性を高めるデザインパターンです。

実際にTreeViewコンポーネントにRender Propsパターンを使ってみましょう。

まずはTreeViewコンポーネントでPropsから「Nodeに表示する内容」を受け取れるようにします。

export type TreeNode = {
  id: number;
  name: string;
  children: TreeNode[]; // 再帰的な構造
};

type TreeViewProps = {
  nodes: TreeNode[];
  // Render Propsパターンの採用
  // nodeの詳細部分の表示方法は利用側から受け取る
  renderNode: (nodeId: TreeNode["id"]) => React.ReactNode;
};

export const TreeView: React.FC<TreeViewProps> = ({ nodes, renderNode }) => {
  return (
    <ul>
      {nodes.map((node) => (
        <TreeBranch key={node.id} node={node} renderNode={renderNode} />
      ))}
    </ul>
  );
};

type TreeBranchProps = {
  node: TreeNode;
  renderNode: (nodeId: TreeNode["id"]) => React.ReactNode;
};

const TreeBranch: React.FC<TreeBranchProps> = ({ node, renderNode }) => {
  const hasChildren = node.children.length > 0;

  return (
    <li>
      {/* ノード本体 */}
      <div>
          <p>{node.name}</p>
          
          {/* 外部から差し込む表示 */}
          {renderNode(node.id)}
      </div>

      {/* 子ノードの再帰的なレンダリング */}
      {hasChildren && (
        <ul>
          {node.children.map((child) => (
            <TreeBranch key={child.id} node={child} renderNode={renderNode} />
          ))}
        </ul>
      )}
    </li>
  );
};

利用側のイメージも書いてみます。

export const Top: React.FC = () => {
  return (
      <TreeView
          nodes={nodes}
          renderNode={(nodeId) => 
              <NodeDetail nodeId={nodeId} />
          }
      />
  );
};

type NodeDetailProps = {
    nodeId: TreeNode["id"];
};

const NodeDetail: React.FC<NodeDetailProps> = ({ nodeId }) => {
  // ノードの詳細をgetするAPIなどが内包されている
  const{ isLoading, data } = useNodeDetail(nodeId);

  return (
      <>
          {/* ノードの説明をAPIから取得して表示する */}
          {isLoading && <p>…loading</p>}
          {!isLoading && <p>{data.description}</p>}
      </>
  );
};

このように、Render Propsパターンを採用することで利用側から「どんなUIで表示するのか」や「どのAPIを叩くのか」などを自由に渡せるようになりました。

TreeView側は「ツリー構造の描画」のみに集中することができます。

Render Propsパターンの注意点

公式で「多くのユースケースでは、カスタムフックに置き換わりました」と言及されている

Reactの旧版公式ドキュメントでは下記のような記述があります。

レンダープロップはモダンな React でも使われますが、あまり一般的ではなくなっています。 多くのユースケースでは、カスタムフックに置き換わりました。

カスタムフックが登場する前は、Render Propsがロジックの再利用のためにも使われていました。
(特に従来はライブラリから提供できる再利用単位がコンポーネントしかなかったので、Render Propsパターンなどを使う必要があったようです。)
その流れから「多くのユースケースはフックに置き換わった」とされています。

ロジックの「再利用」を目的にRender Propsを多用すると、Patten.devのHooksのセクションで示されているように、コールバックのネストが発生し、可読性や保守性が低下してしまいます。
カスタムフックがある今は、そのデメリットが目立ってしまいます。

一方で、本記事のようにロジックやUIの責務を「分離」したいユースケースではいまだにRender Propsが有効なこともあるようです。

最後に

この記事では、ツリー構造UIの再利用性を高めるために、Render Propsパターンを使って責務の分離をすることについてまとめました。
記事でも取り上げたPatterns.devには他にJavaScriptやReactのデザインパターンがまとめられていたので、一通り読んでみようと思います!

参考記事

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?