【ITニュース解説】React Context Is Not State Management: Stop Using It
2026年09月09日に「Dev.to」が公開したITニュース「React Context Is Not State Management: Stop Using It」について初心者にもわかりやすく解説しています。
ITニュース概要
React Contextは依存性注入ツールであり、高頻度で更新される状態管理に使うとパフォーマンスが著しく低下する。Contextの値が変わると関係ないコンポーネントまで再レンダリングされ、アプリケーションが重くなるためだ。テーマなど低頻度データに限定し、高速な状態管理にはZustandなどの外部ライブラリの利用が推奨される。
ITニュース解説
現代のReact開発では、「Prop Drilling(プロップドリリング)」という問題がよく話題になる。これは、親コンポーネントから非常に深い階層の子コンポーネントまで、データを何度も経由させて渡していく必要がある状況を指す。この問題は開発の効率を大きく低下させるため、多くの開発チームは手軽に利用できるReact Contextを使って解決しようとする。ContextはReactに標準で組み込まれており、使い方も簡単なため、深くネストされたコンポーネントツリーにデータを渡す問題を解決してくれるように見えるからだ。
しかし、多くの大規模なReactアプリケーションのコードベースを調査すると、Contextの誤った使い方によって密かにアプリケーションのパフォーマンスが低下している共通のパターンが見つかっている。根本的な真実として、React Contextは「依存性注入(Dependency Injection)」のためのツールであり、決して「状態管理(State Management)」のためのツールではない。間違った目的でContextを使うと、コードが複雑になるだけでなく、最終的にはアプリケーションの「メインスレッド」を詰まらせてしまうパフォーマンス上のボトルネックを生み出すことになる。メインスレッドが詰まると、ユーザーインターフェース(UI)の動作が遅く、反応が悪く感じられるようになる。
Contextが高頻度で更新される状況でなぜ失敗するのかを理解するには、Reactがどのように「再レンダリング(Re-render)」を処理するかを知る必要がある。Reactのコンポーネントは、プロパティや状態が変更されると再レンダリングされ、UIが更新される。Contextの場合、Context.Providerによって提供される値が変更されると、そのContextを「消費する(Consume)」、つまりそのContextの値を使っているすべてのコンポーネントにReactが変更を通知し、再レンダリングを強制する。
ここで重要なのは、このプロセスがReact.memo(コンポーネントの再レンダリングを最適化するための機能)を「バイパスする」という点だ。もしあるコンポーネントがContextを消費している場合、そのコンポーネントが実際に必要としている特定のデータが変わっていなくても、Contextの値が変更されるたびに再レンダリングされてしまうのだ。これは非常に非効率的で、無駄な処理を大量に発生させる。
このパフォーマンス低下をさらに悪化させる最もよくある間違いは、オブジェクトリテラルを直接プロバイダーに渡す「インラインオブジェクト」というアンチパターンだ。例えば、次のようなコードだ。
<Context.Provider value={{ state, dispatch }}>
{children}
</Context.Provider>
Reactはオブジェクトの比較に「参照の等価性(Reference Equality)」という方法を用いる。これは、オブジェクトの中身が同じであっても、メモリ上の参照アドレスが異なれば別々のオブジェクトとみなす、という考え方だ。上記のコードでは、親コンポーネントが再レンダリングされるたびに{ state, dispatch }という新しいオブジェクトがその都度作成される。たとえstateの中身が全く変わっていなくても、新しく作られたオブジェクトは前のオブジェクトとは別の参照を持つため、Reactは値が「変更された」と判断してしまう。これにより、コンポーネントツリー全体で大規模で不要な再レンダリングの連鎖が強制的に引き起こされ、アプリケーションは目に見えて遅くなる。
実際に、ある企業ダッシュボードアプリケーションで、50以上の入力フィールドを持つ複雑なフォームが監査された事例がある。ユーザーからは、フィールドに文字を入力するたびに動作が重く、反応が悪いという報告があった。アプリケーションのパフォーマンスプロファイリングを行った結果、1回のキー入力に対する更新遅延が驚くべき250ミリ秒にも達していることが判明した。これは、ユーザーがキーボードを叩いてから画面に文字が表示されるまでに、およそ4分の1秒もかかっていることを意味する。
この問題の解決策として、高頻度で更新されるUIの状態を、一つの大きなReact Contextで管理するのをやめ、「Zustand(ズースタンド)」という外部の状態管理ライブラリに移行した。Zustandは「アトミックでセレクターベースの購読(Atomic, Selector-based Subscriptions)」という仕組みを利用している。これにより、ユーザーが特定の入力フィールドに文字を入力したとき、そのフィールドに関連するごく特定のコンポーネントだけが再レンダリングされる。その結果、更新遅延は250ミリ秒から、非常に滑らかな12ミリ秒へと劇的に改善された。これは、Contextの誤用がどれほどパフォーマンスに悪影響を与えるか、そして適切なツールを使うことでどれほど改善できるかを示す明確な証拠だ。
React Context自体は「悪い」ツールではない。しかし、それは特定の目的に特化したツールであるということを理解しておく必要がある。Contextが真価を発揮するのは、「低頻度でしか変更されないグローバルなデータ」を管理する場合だ。
開発のアーキテクチャを設計する上で推奨される基本的なルールは以下の通りだ。
まず、低頻度データのみに利用する。UIのテーマ(ダークモードとライトモードの切り替え)、ユーザーの認証状態(ログインしているかどうか)、ロケール設定(言語や地域設定)など、ほとんど変更されない、または変更されても影響範囲が限定的なデータにContextを使用するべきである。これらのデータは、頻繁な更新によるパフォーマンスの問題を引き起こしにくい。
次に、Contextを分割する。もし何らかの理由でContextを状態管理のために使わなければならない場合でも、Contextを細かく分割することが非常に重要だ。例えば、アプリケーションの状態(StateContext)を管理するContextと、状態を更新するための関数(DispatchContext)を管理するContextを別々に用意する。これにより、状態の変更に関心のあるコンポーネントだけがStateContextを購読し、状態を更新する関数を呼び出すことだけに関心のあるコンポーネントはDispatchContextを購読すれば良い。こうすることで、状態が変更されても、更新関数を呼び出すだけのコンポーネントは再レンダリングされずに済む。
そして、高頻度データには外部ストアを利用する。キーボードの入力、マウスの動き、リアルタイムなデータストリームなど、非常に高頻度で変更されるあらゆるデータについては、その状態をZustandやJotai(ジョタイ)といった外部の状態管理ライブラリに移動させるべきだ。これらのライブラリは、内部的にuseSyncExternalStoreというReactのフックを利用している。これは、グローバルな再レンダリングを引き起こすことなく、パフォーマンスが高く、予測可能な方法で状態を管理するための強力な手段だ。これにより、本当に必要なコンポーネントだけを効率的に更新できる。
フロントエンドアプリケーションの複雑さは年々増しており、2025年になってもこの傾向は続いている。だからこそ、私たちはアーキテクチャの選択に対してより規律を持つ必要がある。React Contextを、あらゆる問題を解決する万能なツールとして安易に使うのはやめるべきだ。あなたのアプリケーションを使うユーザーは、滑らかで反応の良いインターフェースを体験する権利がある。そして、アプリケーションのメインスレッドも、不必要な処理から解放され、効率的に動作するべきなのだ。Contextの適切な理解と使用は、パフォーマンスとユーザー体験を大きく左右する重要な要素である。