React には、HOC や Custom Hook など、コンポーネントとデータ、ロジックを再利用するための様々な手法があります。今回は、その中から render props パターンについて紹介します。
Render Props パターンとは?
非常に簡単に言うと、Render Props はコンポーネントの prop として JSX を返す関数を渡し、何をレン더リングするかを親(外部)に委譲するパターンです。簡単な例を見ると次のようになります。(ちなみに、props の名前が必ずしも render である必要はありません。)
// (1) 枠組みを提供するコンポーネント
const Box = ({ render }) => {
return (
<div style={{ border: '2px solid blue', padding: '20px', borderRadius: '8px' }}>
{render()}
</div>
);
};
// (2) 利用側: 「テキストを入れたい」
<Box render={() => <span>シンプルなテキストです。</span>} />
// (3) 利用側: 「画像を入れたい」
<Box render={() => <img src="logo.png" alt="ロゴ" />} />このように、JSX を返す関数を props として受け取り、それを呼び出すパターンです。非常にシンプルなので、これだけではこのパターンがなぜ有用なのか、どのように使えるのかがまだ分かりにくいかもしれません。
データを渡す Render Props
もう少し発展させてみましょう。例えば、子コンポーネントで特定のデータを管理しているが、親によってそのデータの見せ方を変えたい場合があります。
// (1) データを保持しているコンポーネント
const UserProvider = ({ render }) => {
const user = { name: "Sanghyeon", role: "Developer" };
// 保持しているデータを render 関数に渡して実行
return <div>{render(user)}</div>;
};
// (2) 利用側 A: 名前を大きく表示したい場合
<UserProvider render={(user) => <h1>{user.name}</h1>} />
// (3) 利用側 B: 役割と名前を一緒に表示したい場合
<UserProvider render={(user) => <p>{user.name} ({user.role})</p>} />このように、コンポーネントが持つデータを外部(親)で自由に取り扱いながらレンダリングできます。
children prop の活用
上の例では render という名前の prop を使用しましたが、children prop を活用するのがより一般的です。この方法を使うと、一般的なコンポーネントのように子要素を囲む形になり、可読性が向上します。
// (1) render prop の代わりに children を関数として実行
const UserProvider = ({ children }) => {
const user = { name: "Sanghyeon", role: "Developer" };
return <div>{children(user)}</div>;
};
// (2) 利用側: より直感的な構造になります。
<UserProvider>
{(user) => (
<div>
<h1>{user.name}</h1>
<p>{user.role}</p>
</div>
)}
</UserProvider>Hooks による代替
しかし、現在のコードも Hook で十分に代替可能であり、単にデータを扱うだけであれば Hook を使用する方がよりクリーンでシンプルになる場合があります。
// Hook に書き換えたコード
const useUser = () => {
const user = { name: "Sanghyeon", role: "Developer" };
return user;
};
// (1) 利用側 A: 名前を大きく
function LargeName() {
const user = useUser();
return <h1>{user.name}</h1>;
}
// (2) 利用側 B: 役割と名前
function NameAndRole() {
const user = useUser();
return <p>{user.name} ({user.role})</p>;
}最近の React 開発では、多くの Render Props パターンが Hook に置き換えられました。しかし、Hook では解決が難しい領域があります。それは、**「ロジックに特定の DOM 構造が結合される必要がある場合」**です。
実践的な活用例
無限スクロールを実装してみましょう。無限スクロールを実装する方法はいくつかありますが、ここでは IntersectionObserver とスクロールの終わりを検知するダミーコンポーネントを利用する方法で実装するとします。
function PostListContainer() {
const { items, fetchNextPage, hasMore, isLoading } = useInfiniteScrollHook();
const observerRef = useRef(null); // 開発者が直接管理
return (
<div>
<PostList items={items} />
// (1) 不便な点: データ(isLoading)に応じた UI 処理をここで毎回手動で行う必要がある
{isLoading && <MyCustomSpinner />}
// (2) 不便な点: ロジックのための「ダミー div」を忘れずに手動で入れる必要がある
{hasMore && <div ref={observerRef} style={{ height: '20px' }} />}
</div>
);
}Hook はロジックのみを提供するため、ref と DOM の接続は開発者が手動で行う必要があります。このようなロジックが繰り返される場合、かなり面倒な作業になるでしょう。
Render Props 方式への修正
// (1) Render Props コンポーネント
function InfiniteScroll({ onLoadMore, hasMore, children }) {
const [isLoading, setIsLoading] = useState(false);
const observerRef = useRef(null);
useEffect(() => {
if (!hasMore || isLoading) return;
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setIsLoading(true);
onLoadMore().finally(() => setIsLoading(false));
}
});
if (observerRef.current) observer.observe(observerRef.current);
return () => observer.disconnect();
}, [hasMore, onLoadMore, isLoading]);
return (
<div>
// 子関数に現在のローディング状態を渡す
{children(isLoading)}
// 必須のダミー要素はコンポーネントが責任を持つ
{hasMore && <div ref={observerRef} style={{ height: '20px' }} />}
</div>
);
}
// (2) 利用側: 内部状態である isLoading を受け取って UI を決定する
<InfiniteScroll hasMore={hasMore} onLoadMore={fetchNextPage}>
{(isLoading) => (
<>
<PostList items={items} />
// ローディングバーをリストのすぐ下に置くか、右上に小さく表示するかを親が決定する
{isLoading && <MyCustomSpinner />}
</>
)}
</InfiniteScroll>Render Props パターンを利用すると、ロジックと必須の DOM 構造を一式として提供できます。利用側は observerRef を気にする必要がなくなり、isLoading を children prop として渡すことで、外部からどのように表示するかをコントロールできるようになります。
メリットとデメリット
メリット
- ロジックと構造の完全なカプセル化:
InfiniteScrollの例のように、ロジックを動かすために特定の DOM 要素(ダミー div など)が必ず必要な場合、それをコンポーネント内部に隠蔽して再利用性を最大化できます。 - 制御権の委譲: データはコンポーネントが管理しますが、「どのように見せるか」は利用する親が 100% 決定します。そのおかげで、非常に柔軟な UI 構成が可能になります。
デメリット
- Render Prop Hell (ネストの問題): 複数の Render Props を入れ子にして使用する場合、コードの階層が深くなり、可読性が急激に低下する可能性があります。
- Hook に比べて冗長なコード: 単純なロジック共有であれば、Hook を使用するよりも記述量が増え、構造が複雑に見えることがあります。
まとめ
単にデータやロジックを再利用する目的であれば、Hook で十分です。しかし、ロジックと DOM 構造までカプセル化したい場合は、Render Props パターンを検討してみてください。
それでもまだ使い道がピンとこない場合は、Render Props パターンを活用したライブラリ集があるので、そこからインスピレーションを得てみましょう!
参照