すべてのReact開発者は、最終的に同じ問いに直面します。「Contextを使うべきか、それともこれはReduxで解決すべき問題なのか?」と。開発を始めて数ヶ月の人にとって、オンライン上の喧騒は、それが二者択一の決断であるかのように聞こえるかもしれません。Reduxをレガシーな負債として扱うチュートリアルもあれば、ContextはTodoリスト程度の規模を超えてスケールできないと警告するものもあります。どちらの極端な意見も、あまり役に立ちません。真実は、これらのツールは異なる種類の悩みを解決するためのものであり、賢明な選択はアプリケーションが実際に何を行うかに依存するということです。

Prop Drillingの問題

状態管理の戦略を選ぶ前に、両方のツールが解決しようとしている問題(病)を理解しておくと役立ちます。例えば、eコマースサイトを構築しているとしましょう。トップレベルのAppコンポーネントでユーザーのプロフィールを取得します。その下のフッターにある小さなAccountLinkコンポーネントが、そのプロフィール画像を必要としています。グローバルなストアがなければ、userオブジェクトはHome、次にHeader、NavContainer、UserDropdownを経て、最終的にAccountLinkへと渡されなければなりません。その間にあるすべてのレイヤーが、自分では使用しないデータに触れることになります。これがProp Drillingです。

Prop Drillingはコンポーネントを脆くします。中間にいるコンポーネントを一つ取り除くだけでチェーンが壊れてしまうため、リファクタリングがリスクになります。また、コンポーネントが単に下へと渡すためだけのpropsを要求することになるため、再利用性も損なわれます。ContextとReduxはどちらも、遠く離れたコンポーネントが共有データに直接サブスクライブできるようにすることで、これを解消します。しかし、データの届け方や、その際にかかるコストは、すぐに分かれていきます。

React Context APIが適している場合

React Contextはライブラリ自体に組み込まれています。追加のnpmインストールも、ビルド設定も、ボイラープレートファイルも必要ありません。コンテキストオブジェクトを作成し、ツリーの一部をProviderでラップし、ネストされた任意のコンポーネントでuseContextを使ってその値を消費します。そのシンプルさゆえに、Contextは、状態の変化が頻繁ではなく、その状態の形が比較的フラットな、小規模から中規模のプロジェクトで真価を発揮します。

UIテーマを考えてみてください。ユーザーがライトモードとダークモードを切り替えるのは、おそらく1セッションに一度程度です。値はすべてのstyledコンポーネントに伝播しますが、変化が非常に稀であるため、パフォーマンスへの懸念はほとんど生じません。認証ステータスも、典型的な適応例です。一度ユーザーがログインすると、isAuthenticatedフラグとuserオブジェクトは、数十回のページ遷移の間、安定したままです。言語やローカライズの設定も同様に動作します。これらは、多くのコンポーネントが必要とするものの、変化させるコンポーネントはほとんどない、広範で動きの遅いシグナルです。

問題は、Contextがどのように更新を処理するかです。Context Providerの値が変更されると、Reactはそのコンテキストを消費しているすべてのコンポーネントを再レンダリングします。小規模なアプリケーションでは、それを感じることはないでしょう。しかし、大規模なアプリケーションにおいて、頻繁に変化するデータを広く使用されているContextの中に置いてしまうと、無駄な再レンダリングの連鎖を引き起こしてしまいます。変化の激しい部分を分離するためにコンテキストを分割することもできますが、そうなると、別のツールがすでに解決している最適化のための回避策を、手動で設計していることになります。

Redux Toolkitが真価を発揮する場合

Redux Toolkitは、状態が複雑で、更新が頻繁に行われ、複数の離れた機能が衝突することなく同じデータを読み書きする必要があるアプリケーション向けに設計されています。ショッピングカートを例に考えてみましょう。ユーザーが商品カードからアイテムを追加します。ヘッダーのカートアイコンはバッジのカウントを更新しなければなりません。サイドバーがスライドして、アイテムのリストを表示します。割引コードの入力欄はバリデーションを実行します。その後、チェックアウトページがカートの内容を読み取ります。その状態は、ツリー全体の無関係なコンポーネントによって触れられ、頻繁に変化します。

Redux Toolkitは、中央集権的なストアと、明示的なステートのスライス(slices)を通じてこれを解決します。コンポーネントはuseSelectorを使用して、自分が必要なデータの断片にのみサブスクライブします。リアルタイムのダッシュボードで株価が更新されても、ユーザーのプロフィール設定を表示しているコンポーネントは起動しません。Reduxは内部で参照の等価性チェックを使用しているため、サブスクリプションが粒度の細かいものになります。これは、コンポーネント数が数百に増えてくると極めて重要になります。

また、Reduxは予測可能なデータフローを提供します。状態の変化は、reducerによって処理されるdispatched actionを通じて行われます。これは専門用語のように聞こえるかもしれませんが、実際には、コードベースをaddToCartでgrepすれば、カートを修正しているすべてのコードパスを見つけられるということを意味します。大規模なチームにおいて、この規約はバグを防ぎます。対照的に、Contextは単なる値とセッターに過ぎません。どのコンポーネントからでもsetStateを呼び出すことができ、不正な値の発生源を突き止めるには、複数のコンポーネントにまたがってブレークポイントを設置しなければなりません。

両者の決定的な違い

パフォーマンス特性は、これら2つのツールを分かつ最も大きな要因です。Contextは、新しい値をすべてのコンシューマーに対して無条件にブロードキャストします。一方、Reduxは、選択されたスライスが変更されたサブスクライバーにのみ通知を行います。もし、毎秒株価が更新されるリアルタイムの株価ダッシュボードを構築する場合、Contextを使用するとグローバルな再レンダリングの嵐が発生してしまいます。Reduxであれば、ティッカーセルとスパークラインチャートのみを再計算させることができます。

デバッグにおいても、複雑なアプリケーションではReduxが優位に立ちます。Redux DevToolsを使えば、タイムトラベル・デバッグが可能です。ディスパッチされた各アクションを遡り、状態が巻き戻される様子を確認できます。配送料の計算、決済の検証、エラーリカバリを含む多段階のチェックアウトフローにおいて、バグを引き起こした正確なシーケンスを再現できることは非常に価値があります。Contextは標準のReact DevToolsに依存します。現在のContextの値を検査することはできますが、組み込みのアクションログや状態の差分(state diff)ビューアはありません。結局、console.logをあちこちに散りばめることになります。

ミドルウェアとサイドエフェクトは、ReduxのDNAの一部です。Redux ToolkitにはcreateAsyncThunkが含まれており、データフェッチライブラリともスムーズに統合できます。API呼び出しのオーケストレーション、ローディングスピナーの表示、ネットワークエラーの処理、そして結果のキャッシュを、すべてReduxのデータフロー内で行うことができます。Contextには、非同期ロジックのための組み込みパターンがありません。コンポーネント内でデータをフェッチしてその結果をContextにプッシュするか、自作の非同期ユーティリティでProviderをラップするかのどちらかになります。これでも動作はしますが、アドホック(場当たり的)な対応になります。

セットアップコストに関しては、Contextの完全な勝利です。テーマProviderを作成するのにかかる時間は、わずか5分程度です。Redux Toolkitでは、storeファイルの作成、sliceの定義、そしてアプリケーションをProviderでラップする必要があります。かつてのReduxのように、膨大なボイラープレートを前にして1週間も儀式を行うようなことはありませんが、それでもContextよりはセットアップの手間がかかります。週末のサイドプロジェクトや、ルートが3つしかないようなダッシュボードであれば、そのオーバーヘッドは見合わないかもしれません。

同一アプリケーション内での併用

どちらか一方の陣営に忠誠を誓う必要はありません。多くのプロダクションアプリケーションでは、グローバルなUIシェルに関する事項にはContextを、ドメインに密接したビジネスデータにはReduxを使用しています。一般的なパターンは、テーマ、ロケール、そしておそらく軽量な認証フラグなどをContextに保持することです。これらはすべてのルートで必要とされ、変更も稀だからです。一方で、頻繁な更新やコンポーネントを跨ぐロジックによって精密な制御が求められる注文管理システム、通知センター、データテーブルなどはReduxで管理します。

このハイブリッドなアプローチにより、静的なテーマオブジェクトに対して無理にフル機能のRedux storeを適用することなく、簡単なことは簡単なままにしておくことができます。また、そもそも本格的な状態管理を必要としないUI要素によって、Reduxのsliceが埋め尽くされることも防げます。

重要なポイント

より重厚なツールを選んだからといって、名誉が与えられるわけではありません。まず、状態がどの程度の頻度で変化するか、いくつのコンポーネントがそれに触れるか、そしてチームの境界を越えて変更を追跡する必要があるかどうかを確認することから始めてください。中規模のアプリで、変化が少なく広く共有される値を管理しているのであれば、おそらくContextで十分です。もし状態が頻繁に変化し、関連性のない機能にまたがり、明確な監査証跡(audit trail)が必要な場合は、Redux Toolkitが苦労を軽減してくれるでしょう。

カンファレンスの講演やGitHubのスター数ではなく、プロジェクトの形態に基づいて選択してください。商品が50個入るショッピングカートだからといって、自動的にReduxが必要になるわけではありませんし、テーマの切り替えにグローバルなstoreは必要ありません。ツールを問題に適合させれば、ハイプサイクルが過ぎ去った後も、コードベースの保守性を長く保つことができます。