この記事は株式会社カオナビ Advent Calendar 2025の16日目です。
はじめに
私はReactでツリー構造のUIを作成しています。
このUIはいずれ複数の箇所で利用する可能性がありそうなので、再利用しやすい状態にしたいと考えています。
この記事では、開発中のツリー構造UIの再利用性をあげるために使用した「Render Propsパターン」について紹介します。
作っているUIのイメージ
下記のようなイメージでツリー構造のUIを開発しています。
ツリー構造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から得た詳細情報を表示させたいということになりました。

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のデザインパターンがまとめられていたので、一通り読んでみようと思います!
