React の設計思想

宣言的 UI、コンポーネント合成、一方向のデータフローを通して React の考え方を理解する。

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 を何層も渡すことがあります。途中のコンポーネントが値を使わず受け渡すだけになったときは、次の順で検討します。

  1. コンポーネントの境界を変え、state を利用場所へ近づけられないか
  2. children やコンポーネント合成で、必要な UI を親が渡せないか
  3. 広く共有する安定した値なら Context が適切か

Context は props の受け渡しをなくすためだけの仕組みではありません。テーマ、認証済みユーザー、ルーティング情報のように、多くの場所で同じ意味を持つ値に使うと意図が明確です。

副作用は UI の計算から分離する

レンダーの純粋さは、React が中断、再開、再実行といった処理を安全に行うための土台です。レンダー中は JSX を計算し、イベントハンドラではユーザー操作に反応し、Effect では外部と同期するという分担を守ります。

レンダー: 現在の入力から UI を計算する
イベント: ユーザーの操作に応答する
Effect: React の外部システムと同期する

この境界が曖昧になると、二重送信、無限ループ、古い値を使った処理などが起きやすくなります。

設計を始める順番

画面を作るときは、次の順で考えると React の原則を適用しやすくなります。

  1. データが揃った状態の静的な UI をコンポーネントとして分ける
  2. UI を変化させる最小の state を見つける
  3. state を共有する最も近い親を決める
  4. 子へ props を渡し、更新は callback で上流へ伝える
  5. 外部システムとの同期だけを Effect に分ける

この順番は唯一の正解ではありませんが、API を先に増やすよりも、データの流れを明確にする助けになります。

Sources