Render Props
| 対応: | React 16.8(2019) |
|---|
『React』の Render Props は、コンポーネントのレンダリング内容を外側から制御するための設計パターンです。「関数を props として渡す」ことで、コンポーネント内部のロジック(状態管理・イベント処理など)を再利用しながら、描画する UI は呼び出し側が自由に決められます。
基本構文
// render props パターンの基本形
// render: レンダリング内容を返す関数(props として渡します)
function DataProvider({ render }) {
// ロジック(状態・副作用など)はこのコンポーネントが持ちます
const [value, setValue] = React.useState(null);
// render props を呼び出して描画内容を委譲します
return render(value);
}
// 呼び出し側では描画したい UI を関数で渡します
<DataProvider render={function(value) {
return <p>値: {value}</p>;
}} />
よく使われる渡し方
| 方法 | 概要 |
|---|---|
render prop | 明示的に render という名前の props に関数を渡します。パターンの意図が名前から明確に伝わります。 |
children を関数にする | children として関数を渡す書き方(Function as Children)です。JSX の入れ子として自然に記述でき、可読性が高まります。 |
サンプルコード
マウスカーソルの現在座標を追跡するロジックを持つ MouseTracker コンポーネントが、描画内容を呼び出し側に委ねる例です。
// マウス座標を追跡するコンポーネントです
// render props を通じて、描画内容を呼び出し側に委ねます
function MouseTracker({ render }) {
const [pos, setPos] = React.useState({ x: 0, y: 0 });
// mousemove イベントで座標を更新します
function handleMouseMove(e) {
setPos({ x: e.clientX, y: e.clientY });
}
return (
<div
onMouseMove={handleMouseMove}
style={{ height: '200px', border: '1px solid #ccc', position: 'relative' }}
>
{/* render props を呼び出して描画内容を委譲します */}
{render(pos)}
</div>
);
}
// 猫の画像をマウス座標に追随させるコンポーネントです
function Cat({ x, y }) {
return (
<img
src="https://placekitten.com/60/60"
alt="cat"
style={{
position: 'absolute',
left: x - 30 + 'px',
top: y - 30 + 'px',
pointerEvents: 'none',
}}
/>
);
}
// App コンポーネント(ルートコンポーネント)
function App() {
return (
<div style={{ fontFamily: 'sans-serif', padding: '16px' }}>
<p>枠内でマウスを動かしてください。</p>
{/* render props に関数を渡し、描画内容を呼び出し側で決定します */}
<MouseTracker render={function(pos) {
return <Cat x={pos.x} y={pos.y} />;
}} />
{/* 同じ MouseTracker を使い、別の UI を描画することもできます */}
<MouseTracker render={function(pos) {
return (
<p style={{ padding: '8px' }}>
座標: ({pos.x}, {pos.y})
</p>
);
}} />
</div>
);
}
export default App;
children を関数にする書き方(Function as Children)
render という名前の props を使う代わりに、children を関数として渡す書き方も広く使われます。JSX の入れ子として記述できるため、可読性が高まります。
// children を関数として受け取るバージョンです
function MouseTracker({ children }) {
const [pos, setPos] = React.useState({ x: 0, y: 0 });
function handleMouseMove(e) {
setPos({ x: e.clientX, y: e.clientY });
}
return (
<div
onMouseMove={handleMouseMove}
style={{ height: '200px', border: '1px solid #ccc' }}
>
{/* children を関数として呼び出します */}
{children(pos)}
</div>
);
}
// 呼び出し側: children として関数を渡します
function App() {
return (
<MouseTracker>
{function(pos) {
return <p>座標: ({pos.x}, {pos.y})</p>;
}}
</MouseTracker>
);
}
export default App;
概要
Render Props パターンは、コンポーネントのロジック(状態・副作用・イベント処理など)を再利用しながら、描画する UI を呼び出し側に委ねる設計手法です。同じロジックを持つコンポーネントで、異なる見た目を複数組み合わせたい場合に特に有用です。
注意点として、render props に渡す関数を毎回新しいアロー関数やインライン関数で記述すると、再レンダーのたびに新しい関数参照が生成されます。React.memo でラップされた子コンポーネントが props の変化を検知して不要な再レンダーを起こす場合があるため、パフォーマンスが気になる場面では useCallback で関数をメモ化する方法もあります。
『React』がフック(Hooks)を導入して以降、Render Props で実現していたロジックの多くは カスタムフック に置き換えられています。カスタムフックはコンポーネントのネストが増えないため、コンポーネントツリーがシンプルに保たれます。一方、コンポーネントとして使いたい・JSX の構造の中に自然に組み込みたいという場面では Render Props が今も有効な選択肢です。
関連ページ: カスタムフック / 高階コンポーネント(HOC) / useCallback / React.memo
よくあるミス
render props に毎回新しい関数を渡して React.memo が無効化される
Render Props パターンで render={function(data) { return <Card data={data} />; }} のように render 内でインライン関数を渡すと、レンダリングのたびに新しい関数オブジェクトが生成されます。React.memo でラップした子コンポーネントでも props の参照が変わるため効果がなくなります。useCallback で関数をメモ化する方法もあります。
tracker_ng.jsx
import { memo } from 'react';
const PowerDisplay = memo(function PowerDisplay({ name, power }) {
console.log('render:', name);
return <p>{name}: {power}</p>;
});
function App() {
return (
<PowerTracker
render={function(power) {
// インライン関数のため毎回新しい参照になり memo が無効化される
return <PowerDisplay name="五条悟" power={power} />;
}}
/>
);
}
修正後:
tracker_ok.jsx
import { memo, useCallback } from 'react';
function App() {
// useCallback で関数をメモ化して参照を安定させる
const renderPower = useCallback(function(power) {
return <PowerDisplay name="五条悟" power={power} />;
}, []);
return <PowerTracker render={renderPower} />;
}
children を関数にする書き方で型チェックが難しくなる
Render Props の children を関数にするパターンは、TypeScript や PropTypes での型定義が必要です。children が関数でない場合にランタイムエラーになるため、コンポーネント内で typeof children === 'function' チェックを入れるか、TypeScript で型を明示することが一般的です。
記事の間違いや著作権の侵害等ございましたらお手数ですがこちらまでご連絡頂ければ幸いです。