render-props-pattern について
React の render props パターンを解説する学習スキルです。異なる描画要件を持つコンポーネント間で状態ロジックやデータを共有する方法と、Hooks との使い分けを扱います。
主な機能
- 01コンポーネント間で状態ロジックを共有
- 02render または children props でデータを渡す
- 03HOC の名前衝突を回避
- 04render props と Hooks を比較
活用シーン
- 01複数の表示コンポーネントで入力状態を共有
- 02描画方法の異なるコンポーネント間でロジックを再利用
- 03render props と Hooks の選択
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ patternsdev/ skills --skill render-props-patternインストール後、Claude Code で/render-props-patternを入力して読み込まれたか確認します。
詳細
Render Props パターン
目次
コンポーネントを非常に再利用しやすくするもう1つの方法は、render prop パターンを使うことです。render prop とは、JSX 要素を返す関数を値として持つコンポーネントのプロパティです。コンポーネント自体は、render prop 以外のものは何もレンダリングしません。独自のレンダリングロジックを実装する代わりに、単に render prop を呼び出します。
Title コンポーネントがあるとします。この場合、Title コンポーネントは渡された値をレンダリングする以外、何もする必要はありません。ここでは render prop を使えます!Title コンポーネントでレンダリングしたい値を、render プロパティに渡しましょう。
使用する場面
- レンダリング方法が異なるコンポーネント間で、状態を持つロジックを共有する必要がある場合に使います
- HOC パターンによって名前の衝突や過度に深いネストが発生する場合に役立ちます
使用しない場面
- カスタムフックでこのパターンを置き換えられる場合。フックを使えば、render prop のネストなしで同じロジックを再利用できます
- JSX が深くネストされ、読みづらく保守しにくくなる場合
- 共有するロジックが、単純なユーティリティ関数やフックで十分な場合
手順
- データを受け取って JSX を返す関数を、
renderプロパティ(またはchildrenプロパティ)として渡します - 最近の React コードでは、たいていの場合 render prop よりカスタムフックを優先します
- 明示的な
renderプロパティの代わりに、関数を子要素として渡すパターンを使うと、よりすっきりします - 複数の render prop コンポーネントを深くネストするのは避け、代わりにフックを使う形にリファクタリングします
詳細
<Title render={() => <h1>I am a render prop!</h1>} />
Title コンポーネント内では、呼び出した render プロパティを返すことで、このデータをレンダリングできます!
const Title = (props) => props.render();
render prop と呼ばれていますが、render prop の名前を render にする必要はありません。JSX をレンダリングするプロパティは、どれも render prop と見なされます!
render prop を受け取るコンポーネントは、通常、render プロパティを呼び出すだけではありません。通常は、render prop を受け取るコンポーネントから、render prop として渡す要素へデータを渡します!
function Component(props) {
const data = { ... }
return props.render(data)
}
これで、render prop は引数として渡されたこの値を受け取れるようになりました。
<Component render={data => <ChildComponent data={data} />}
例を見てみましょう!ユーザーが摂氏温度を入力できるシンプルなアプリがあります。アプリには、その温度を華氏とケルビンで表した値が表示されます。
現在、問題があります。状態を持つ Input コンポーネントがユーザーの入力値を保持しているため、Fahrenheit コンポーネントと Kelvin コンポーネントはユーザーの入力値にアクセスできません!
状態のリフトアップ
ユーザーの入力値を Fahrenheit コンポーネントと Kelvin コンポーネントの両方から利用できるようにする方法の1つは、状態をリフトアップすることです。
この場合、Input コンポーネントは状態を持っています。しかし、兄弟コンポーネントの Fahrenheit と Kelvin もこのデータにアクセスする必要があります。状態を持つ Input コンポーネントを使う代わりに、Input、Fahrenheit、Kelvin とつながりのある最も近い共通の祖先コンポーネントまで状態をリフトアップできます。この場合は App コンポーネントです!
function Input({ value, handleChange }) {
return <input value={value} onChange={(e) => handleChange(e.target.value)} />;
}
export default function App() {
const [value, setValue] = useState("");
return (
<div className="App">
<h1>☃️ Temperature Converter 🌞</h1>
<Input value={value} handleChange={setValue} />
<Kelvin value={value} />
<Fahrenheit value={value} />
</div>
);
}
これは有効な解決策ですが、多くの子コンポーネントを扱うコンポーネントがある大規模なアプリケーションでは、状態のリフトアップが難しい場合があります。状態が変化するたびに、データを扱わない子コンポーネントも含め、すべての子コンポーネントが再レンダリングされる可能性があり、アプリのパフォーマンスに悪影響を及ぼすことがあります。
Render props
代わりに、render prop を使えます!render prop を受け取れるように、Input コンポーネントを変更しましょう。
function Input(props) {
const [value, setValue] = useState("");
return (
<>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Temp in °C"
/>
{props.render(value)}
</>
);
}
export default function App() {
return (
<div className="App">
<h1>☃️ Temperature Converter 🌞</h1>
<Input
render={(value) => (
<>
<Kelvin value={value} />
<Fahrenheit value={value} />
</>
)}
/>
</div>
);
}
これで、Kelvin コンポーネントと Fahrenheit コンポーネントの両方がユーザーの入力値にアクセスできるようになりました!
子要素としての関数
通常の JSX コンポーネントに加えて、React コンポーネントの子要素として関数を渡すことができます。この関数は children プロパティを通じて利用できます。技術的には、これもレンダープロップです。
Input コンポーネントを変更してみましょう。render プロップを明示的に渡す代わりに、Input コンポーネントの子要素として関数を渡します。
export default function App() {
return (
<div className="App">
<h1>☃️ Temperature Converter 🌞</h1>
<Input>
{(value) => (
<>
<Kelvin value={value} />
<Fahrenheit value={value} />
</>
)}
</Input>
</div>
);
}
Input コンポーネントで利用できる props.children プロパティを通じて、この関数にアクセスできます。ユーザー入力の値を props.render に渡して呼び出す代わりに、props.children に渡して呼び出します。
function Input(props) {
const [value, setValue] = useState("");
return (
<>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Temp in °C"
/>
{props.children(value)}
</>
);
}
これで、Kelvin コンポーネントと Fahrenheit コンポーネントは、render プロップの名前を気にすることなく値にアクセスできます。
フック
場合によっては、レンダープロップをフックに置き換えられます。その好例が Apollo Client です。
Apollo Client を使う方法の一つは、Mutation コンポーネントと Query コンポーネントを使うことです。Mutation コンポーネントから、データを必要とする要素にデータを渡すには、子要素として関数を渡します。この関数は、引数を通じてデータの値を受け取ります。
<Mutation mutation={...} variables={...}>
{addMessage => <div className="input-row">...</div>}
</Mutation>
レンダープロップパターンは今でも使用でき、高階コンポーネントパターンよりも好まれることがよくありますが、欠点もあります。
欠点の一つは、コンポーネントの深いネストです。複数のミューテーションやクエリにアクセスする必要がある場合、複数の Mutation コンポーネントや Query コンポーネントをネストできます。
<Mutation mutation={FIRST_MUTATION}>
{(firstMutation) => (
<Mutation mutation={SECOND_MUTATION}>
{(secondMutation) => (
<Mutation mutation={THIRD_MUTATION}>
{(thirdMutation) => (
<Element
firstMutation={firstMutation}
secondMutation={secondMutation}
thirdMutation={thirdMutation}
/>
)}
</Mutation>
)}
</Mutation>
)}
</Mutation>
フックのリリース後、Apollo は Apollo Client ライブラリにフックのサポートを追加しました。Mutation と Query のレンダープロップを使う代わりに、開発者はライブラリが提供するフックを通じてデータに直接アクセスできるようになりました。
useQuery フックを使うことで、コンポーネントにデータを渡すために必要なコードの量を減らせました。
メリット
レンダープロップパターンを使うと、複数のコンポーネント間でロジックやデータを簡単に共有できます。render プロップや children プロップを使うことで、コンポーネントを非常に再利用しやすくできます。高階コンポーネントパターンも主に再利用性とデータの共有という同じ課題を解決しますが、レンダープロップパターンは HOC パターンを使うことで発生する可能性のある問題の一部も解決します。
HOC パターンを使うと発生する可能性のある名前の衝突の問題は、レンダープロップパターンでは起こりません。プロップを自動的にマージしないためです。親コンポーネントが提供する値とともに、子コンポーネントへプロップを明示的に渡します。
プロップを明示的に渡すことで、HOC の暗黙的なプロップの問題も解決できます。要素に渡すプロップはすべて、レンダープロップの引数リストに表示されます。そのため、特定のプロップがどこから来たのかを正確に把握できます。
レンダープロップを通じて、アプリのロジックとコンポーネントのレンダリングを分離できます。レンダープロップを受け取るステートフルなコンポーネントは、データをステートレスなコンポーネントに渡すことができます。ステートレスなコンポーネントは、データをレンダリングするだけです。
デメリット
レンダープロップで解決しようとしていた問題の多くは、React フックに取って代わられています。フックによってコンポーネントに再利用性やデータ共有を追加する方法が変わったため、多くの場合、レンダープロップパターンを置き換えられます。
render プロップにはライフサイクルメソッドを追加できないため、受け取ったデータを変更する必要のないコンポーネントでのみ使用できます。