【ITニュース解説】React Context APIs, State sharing and Zustand
2025年09月04日に「Dev.to」が公開したITニュース「React Context APIs, State sharing and Zustand」について初心者にもわかりやすいように丁寧に解説しています。
ITニュース概要
Reactでコンポーネント間のデータ共有は、深くネストすると複雑化する。Context APIが解決策だが、大規模アプリには再レンダリングなど課題も残る。Zustandのような状態管理ツールを使えば、不要な再レンダリングを減らし、効率的にデータ共有が可能になる。
ITニュース解説
ReactでWebアプリケーションを開発する際、コンポーネント間でデータを共有する方法は非常に重要です。通常、親コンポーネントから子コンポーネントへデータを渡すには「props(プロップス)」という仕組みを使います。これは、親が子に直接データを「渡す」という直感的な方法で、小規模なアプリケーションでは問題なく機能します。
しかし、アプリケーションの規模が大きくなり、コンポーネントが何層にも深くネストされるようになると、このpropsを使ったデータ共有が課題となります。例えば、A、B、Cという3つのコンポーネントがあり、Aが親、BがAの子、CがBの子という関係だとします。もしCがAの持つデータを利用したい場合、AはBにデータを渡し、BはCにそのデータを渡す必要があります。B自身はそのデータが不要であっても、Cに届けるための中継役を果たさなければなりません。このような状況は「props drilling(プロップス・ドリリング)」と呼ばれ、開発を複雑にし、コードの可読性を低下させるだけでなく、不必要な再レンダリングを引き起こしパフォーマンスにも悪影響を与える可能性があります。
このprops drillingの問題を解決するために、Reactは「Context API(コンテキスト・エーピーアイ)」という機能を提供しています。Context APIを使うと、手動でpropsを各レベルで渡すことなく、アプリケーション内の異なるコンポーネント間で値を共有できるようになります。
Context APIの基本的な使い方を見てみましょう。まず、createContextという関数を使ってコンテキストを初期化します。ここではテーマ(lightまたはdark)とその切り替え関数を共有したいので、ThemeContextという名前で、デフォルト値(lightというテーマと空のtoggle関数)を指定して作成します。
次に、このThemeContextの値を実際に提供する「ThemeProvider」コンポーネントを作成します。ThemeProviderの中では、ReactのuseStateフックを使ってthemeという状態変数を管理し、toggle関数でthemeの値をlightとdarkで切り替えるロジックを実装します。そして、ThemeContext.Providerという特殊なコンポーネントを使い、そのvalueプロパティにthemeとtoggle関数を渡します。ThemeProviderはchildren(子コンポーネント)を受け取り、そのchildrenをThemeContext.Providerでラップすることで、children以下に存在するすべてのコンポーネントがThemeContextの値を参照できるようになります。
最後に、useThemeというカスタムフックをエクスポートします。これはuseContextフックを内部で呼び出し、ThemeContextから値を取得するためのものです。このカスタムフックを使うことで、各コンポーネントはThemeProviderに直接ラップされていなくても、コンテキストの値に簡単にアクセスできるようになります。
実際に利用する際は、アプリケーションのルートコンポーネント(例えばApp.tsx)でThemeProviderをインポートし、データ共有したいコンポーネント(ここではThemeSwitchなど)をThemeProviderで囲みます。
ThemeSwitchコンポーネントでは、先ほど作成したuseThemeカスタムフックを呼び出すだけで、現在のテーマthemeとテーマを切り替える関数toggleを取得できます。ボタンのクリックイベントにtoggle関数を設定すれば、ボタンを押すたびにテーマが切り替わるようになります。
さらに、DemonstrateThemeChangeという別のコンポーネントを作成し、そこでもuseThemeからthemeだけを取得して、その値に応じて背景色を変えるようにします。DemonstrateThemeChangeコンポーネントはThemeProviderの範囲内であれば、ThemeSwitchが変更したテーマの値にアクセスし、自身を再レンダリングして見た目を変更します。このように、直接propsを渡さなくても、コンポーネント間で状態が共有され、props drillingの問題が解消されることを確認できます。
Context APIは、確かにprops drillingを解決する強力なツールですが、大規模なアプリケーションで多数の状態を管理するには限界があります。
その限界の一つは、「不必要な再レンダリング」です。もし一つのコンテキストでユーザープロファイル情報とショッピングカート詳細の両方を管理していた場合、ユーザーがカートに新しいアイテムを追加してショッピングカートのデータが更新されると、ユーザープロファイルデータを利用しているコンポーネントも、実際にはプロファイル情報が変わっていないにも関わらず、再レンダリングされてしまいます。
また、「組み込みのセレクタがない」という問題もあります。Context APIには、コンテキスト全体ではなく、その中の一部分だけを購読する機能が組み込まれていません。そのため、ユーザープロファイル情報だけが欲しい場合でも、コンテキスト全体を参照する必要があり、これがコードの複雑さを増す原因となります。
これらの問題を解決しようと、コンテキストを細かく分割していくと、今度はAppコンポーネントがUserProvider、ThemeProvider、CartProviderなど、多数のProviderコンポーネントで深くネストされる「プロバイダ地獄」に陥る可能性があります。これはコードの見た目を悪くし、保守性を低下させます。
Context APIのこのような限界を克服するためには、ReduxやZustandのような専用の状態管理ツールが必要になります。これらのツールは、不必要な再レンダリングを回避し、プロバイダ地獄を解消するためのより高度な機能や最適化を提供します。
記事で紹介されているZustandは、小型で高速、そしてスケーラブルな状態管理ソリューションとして注目されています。
Zustandを使い始めるには、まずnpm install zustandコマンドでライブラリをインストールします。
次に、create関数を使ってストアを作成します。ストアとは、アプリケーション全体で共有される状態とその状態を更新するロジックをまとめたものです。persistミドルウェアをcreate関数と組み合わせることで、作成したストアの状態をブラウザのローカルストレージに保存し、ページをリロードしても状態が失われないようにすることができます。nameプロパティでストアの名前を指定することで、ローカルストレージに保存されるキーが決まります。
TypeScriptを使用している場合、ストア内の状態データの型安全性を確保するために、interfaceを使って状態の型を定義することが推奨されます。例えば、UserShoppingCartインターフェースで商品の情報、Userインターフェースでユーザーのログイン状態やショッピングカートの状態、そしてそれらを変更するための関数を定義します。これらの型をcreate(persist<User>((set) => ({ ... })))のように指定することで、ストアが持つべき状態と関数の構造を厳密に定義できます。
ストアの中では、初期状態としてisLoading、isError、isUserLogedInなどの状態変数を設定し、changeUserLogedInStatusやaddItemToUserShoppingCartといった状態を更新するための関数も定義します。状態を更新する関数の中では、setプロパティを利用します。set関数は、引数として新しい状態オブジェクトを受け取り、ストアの状態を更新します。前の状態に基づいて更新したい場合は、set((prev) => ({ ...prev, userShoppingCart: [...] }))のようにコールバック関数を渡すことで、安全に状態を更新できます。
具体的な利用例として、LoginButtonコンポーネントでは、useUserStoreフックを呼び出してストアからchangeUserLogedInStatus関数とisUserLogedIn状態を取得します。ボタンがクリックされた際にchangeUserLogedInStatusを呼び出し、現在のログイン状態の反対の値を渡すことで、ログイン状態を切り替えます。この変更はpersistミドルウェアによって自動的にローカルストレージに保存されます。
AddToCartButtonコンポーネントも同様にuseUserStoreからaddItemToUserShoppingCart関数を取得し、ボタンクリックでサンプル商品をカートに追加するロジックを実装します。
App.tsxでは、useUserStoreからisUserLogedIn状態を取得し、その値に基づいてAddToCartButtonコンポーネントの表示を条件分岐させます。これにより、ユーザーがログインしている場合にのみ「カートに追加」ボタンが表示されるようになり、アプリケーションのUIが動的に変化します。
このようにZustandを使うことで、アプリケーション全体で共有される状態を効率的に管理し、異なるコンポーネントから簡単にアクセスおよび更新できるようになります。Context APIが抱えていた不必要な再レンダリングやプロバイダ地獄といった問題も回避でき、より大規模で複雑なアプリケーションの開発において、クリーンで保守しやすいコードを保つことが可能になります。