React 状態管理の変遷:Props Drilling から Zustand まで

7 分

はじめに

「状態をどのように美しく管理するか?」これは、プロジェクトが成長する過程ですべての React 開発者が向き合わなければならない中心的な問題です。単純なコンポーネント内部の状態から、複数のコンポーネントにまたがる複雑なグローバル状態まで、React コミュニティはさまざまな解決策を探ってきました。これらの手法の進化は、コンポーネント指向の開発に対する理解が深まってきたことも表しています。

第 1 段階:素朴な時代 — useState と Props Drilling

当初、利用できるのは useState(またはクラスコンポーネントの this.state)だけでした。ある状態を複数のコンポーネントで共有する必要がある場合、React の公式な推奨は 「状態のリフトアップ(Lifting State Up)」 でした。状態をそれらのコンポーネントに最も近い共通の親へ移し、そこから props を通じて状態と更新関数を渡します。

コンポーネントの階層が深くなると、このパターンは 「Props Drilling」 を引き起こします。中間層のコンポーネントが、自身ではまったく利用しない props を、より深い子コンポーネントへ渡すためだけに受け取る状態です。これによってコンポーネント間の結合が強くなり、リファクタリングや保守が難しくなりました。

第 2 段階:公式の回答 — Context API

Props Drilling を解決するため、React は公式に Context API を提供しました。「コンテキスト」を作成し、コンポーネントツリーの上層で値を提供(Provide)すれば、どれほど深い位置の子コンポーネントでも値を直接利用(Consume)でき、階層ごとに手動で渡す必要がなくなります。

// 1. 创建 Context
const ThemeContext = React.createContext('light');

// 2. 在顶层提供值
<ThemeContext.Provider value="dark">
  <App />
</ThemeContext.Provider>

// 3. 在子组件中消费
const theme = useContext(ThemeContext); // 'dark'

しかし、Context API にも「落とし穴」があります。それはパフォーマンスの問題です。Providervalue が変化すると、その Context を利用しているすべてのコンポーネントが再レンダリングされます。たとえ必要なのが value オブジェクトのごく一部だけでも同じです。そのため Context API は、頻繁に変化する複雑なグローバル状態の管理には適していません。

第 3 段階:統一の時代 — Redux

Context API がまだ成熟していなかった時代に Redux が登場し、大規模で複雑なアプリケーションにおける状態管理の事実上の標準へと急速に成長しました。Flux アーキテクチャと関数型プログラミングの考え方を取り入れ、次の原則をもたらしました。

  • 信頼できる唯一の情報源(Single Source of Truth): アプリケーション全体の state を単一の store に保存します。
  • State は読み取り専用: state を変更する唯一の方法は action を dispatch することです。
  • 変更には純粋関数を使う: Reducer は以前の state と action を受け取り、新しい state を返します。

Redux は予測可能性、強力なデバッグツール(タイムトラベル)、豊富なミドルウェアのエコシステムによって、大規模アプリケーションの状態管理問題を解決しました。しかし、欠点も同じくらい明らかでした。煩雑な ボイラープレート(Boilerplate) です。単純な機能を実現するだけでも Actions、Reducers、Dispatchers を記述する必要があり、開発者の認知的な負担は大きくなりました。

第 4 段階:ルネサンス — 軽量で Hooks-First な手法

React Hooks の普及と Redux の複雑さに対する見直しを背景に、コミュニティでは新しく、より軽量な状態管理ライブラリが次々と登場しました。簡潔な API、単純なメンタルモデル、Hooks の能力を十分に活かすことが共通の特徴です。

Zustand はその優れた代表例です。非常に簡単な create 関数で store を作成でき、次の特徴があります。

  • Provider が不要: Store は React コンポーネントツリーの外部に存在し、どこからでもインポートして利用できます。
  • 最小限の API: 1 つの Hook だけで状態へのアクセスと更新ができます。
  • 選択的な購読による高いパフォーマンス: コンポーネントは必要な state の部分だけを購読でき、Context API のパフォーマンス問題を回避します。
const useStore = create(set => ({
  count: 0,
  inc: () => set(state => ({ count: state.count + 1 })),
}));

function Counter() {
  // 只订阅 count 的变化
  const count = useStore(state => state.count);
  return <h1>{count}</h1>;
}

まとめ:銀の弾丸はない、必要に応じて選ぶ

React 状態管理の変遷は、永続的な「最良の手法」はなく、「現在の状況に最も適した手法」だけがあることを教えてくれます。

  • useState: コンポーネントのローカル状態では常に第一の選択肢です。
  • Context API: テーマやユーザー認証情報など、頻繁には変化しないグローバルデータの管理に適しています。
  • Zustand / Jotai: クライアント側のグローバル状態が必要なほとんどのアプリケーションにとって、パフォーマンスと開発体験の絶妙なバランスを提供します。
  • Redux(Redux Toolkit): 厳格なデータフロー規約、複雑なミドルウェア、強力なデバッグ能力が必要な超大規模アプリケーションでは、今も信頼できる選択肢です。