React は、画面を少しずつ命令して作り替えるのではなく、現在の状態から UI を宣言するためのライブラリです。Hooks や JSX はこの目的を実現するための API であり、その背後にはいくつかの一貫した考え方があります。
UI を状態の結果として記述する
React では、UI を props、state、context などの入力から導かれる結果として扱います。
UI = f(props, state, context)
ユーザーが操作して state が変わると、React はコンポーネントを再び呼び出し、新しい UI を計算します。開発者が「このボタンの文言を変更し、この要素を隠す」と個別に DOM を操作する必要は通常ありません。
この見方では、再レンダーは例外ではなく、状態の変化を UI に反映するための通常の手順です。各状態で何を表示すべきかを明確に書くことが、React のコードを読みやすく保つ鍵になります。
コンポーネントを合成する
UI を小さなコンポーネントに分け、それらを組み合わせて大きな画面を作ります。コンポーネントは、見た目の部品であると同時に、関心ごとを分ける単位です。
function ProductPage({ product }: { product: Product }) {
return (
<main>
<ProductHeader product={product} />
<ProductDetails product={product} />
<AddToCartButton productId={product.id} />
</main>
);
}
分割の目的は、ファイルを小さくすることだけではありません。それぞれのコンポーネントが受け取る入力と表示する責務を絞ることで、変更の影響範囲を小さくします。逆に、細かく分けすぎて値を何層も渡すようになったら、構造や state の置き場所を見直す合図になります。
データは上から下へ流す
親コンポーネントは props を通じて子へデータを渡します。この一方向のデータフローにより、ある UI がどの値で決まるかを、コンポーネントツリーの上流へたどって確認できます。
子が親の state を変える必要があるときは、親が callback を props として渡します。
function SearchBox({ query, onQueryChange }: Props) {
return <input value={query} onChange={(event) => onQueryChange(event.target.value)} />;
}
複数のコンポーネントで同じ値を必要とするなら、その共通の親へ state を引き上げます。これを state のリフトアップと呼びます。state を一つの場所で管理すると、表示の食い違いを避けやすくなります。
レンダーを純粋に保つ
レンダー中のコンポーネントは、同じ入力なら同じ JSX を返すように保ちます。レンダー中にタイマーを開始したり、外部の値を書き換えたりすると、React がいつ・何度レンダーしても同じ結果になるという前提が崩れます。
外部システムとの同期は useEffect、ユーザー操作への反応はイベントハンドラで扱います。レンダー、イベント、Effect の役割を分けると、処理がいつ実行されるかを追いやすくなります。
state は必要な場所に置く
state は、その値を必要とするコンポーネントにできるだけ近い場所に置きます。一方で、兄弟コンポーネントが同じ state を共有するなら、共通の親に置きます。
すべてをグローバル state に入れる必要はありません。ローカルに閉じる状態はコンポーネントの近くに置き、画面やアプリ全体で共有する必要がある値だけを上へ移します。この判断が、React アプリケーションの見通しと変更しやすさを大きく左右します。
原則から API を選ぶ
useState は UI に必要な記憶を持つため、useEffect は外部システムと同期するため、Suspense は待機中の UI を境界で表すための手段です。API 名から始めるのではなく、「どの状態が UI を決めるか」「どのコンポーネントが責任を持つか」を先に考えると、React の設計思想に沿った選択ができます。
制御する値と任せる値を分ける
入力欄の値のように React の state を唯一の正本にする設計を、制御されたコンポーネントと呼びます。親が値と更新 callback を渡すため、検証、リセット、他の UI との同期がしやすくなります。
一方、ファイル入力のようにブラウザが値を管理するものや、一時的なフォーカス位置まで常に state にする必要はありません。すべてを制御しようとするのではなく、アプリケーションのロジックが知る必要のある値だけを React のデータフローに乗せます。
Props の受け渡しが深くなったら
一方向のデータフローは理解しやすい反面、遠い子孫へ同じ props を何層も渡すことがあります。途中のコンポーネントが値を使わず受け渡すだけになったときは、次の順で検討します。
- コンポーネントの境界を変え、state を利用場所へ近づけられないか
childrenやコンポーネント合成で、必要な UI を親が渡せないか- 広く共有する安定した値なら Context が適切か
Context は props の受け渡しをなくすためだけの仕組みではありません。テーマ、認証済みユーザー、ルーティング情報のように、多くの場所で同じ意味を持つ値に使うと意図が明確です。
副作用は UI の計算から分離する
レンダーの純粋さは、React が中断、再開、再実行といった処理を安全に行うための土台です。レンダー中は JSX を計算し、イベントハンドラではユーザー操作に反応し、Effect では外部と同期するという分担を守ります。
レンダー: 現在の入力から UI を計算する
イベント: ユーザーの操作に応答する
Effect: React の外部システムと同期する
この境界が曖昧になると、二重送信、無限ループ、古い値を使った処理などが起きやすくなります。
設計を始める順番
画面を作るときは、次の順で考えると React の原則を適用しやすくなります。
- データが揃った状態の静的な UI をコンポーネントとして分ける
- UI を変化させる最小の state を見つける
- state を共有する最も近い親を決める
- 子へ props を渡し、更新は callback で上流へ伝える
- 外部システムとの同期だけを Effect に分ける
この順番は唯一の正解ではありませんが、API を先に増やすよりも、データの流れを明確にする助けになります。