Zenn
🐻

特定の Component tree だけで使える状態管理をZustand + React Contextで実現する

2025/03/25に公開

ご注意!

この記事は私が知りたい内容を AI にまとめさせたものです。あしからず🙇‍♂️

TL;DR(要約)

  • Zustandは軽量で使いやすい状態管理ライブラリだが、アプリ全体ではなく「特定のコンポーネントサブツリー内だけで共有したい状態」にも使いたいことがある。
  • そのような場合、ZustandとReact Contextを組み合わせることで、状態のスコープを適切に制御できる。
  • この記事では、グローバルでない状態管理を実現する実装方法とその利点を解説する。

状態管理の課題:全部グローバルにしたくはない

Reactアプリケーションにおける状態管理では、グローバルステートの扱いがよく議論される。Zustandのようなライブラリを導入すれば、簡潔なAPIで全体の状態を管理できる。

しかし、実際の開発では「一部の状態だけを特定のコンポーネントツリー内で共有したい」というケースが少なくない。例えば、ある画面だけで使うウィザードの状態や、特定のモーダル内部の状態などである。

このような状態をグローバルストアで管理してしまうと、以下のような問題が生じる:

  • props から初期値を受け取ってストアを初期化できない (テストが容易でない)
  • ストアの状態がReactのライフサイクル外で作られるため、副作用的に同期を取る必要がある(例:useEffect

解決策:Zustand + React Contextの組み合わせ

この問題の解決策はシンプルで、Zustandで作ったストアインスタンスをReact Contextに閉じ込めて共有することである。

これにより:

  • ストアの初期化をpropsから行える
  • ストアのスコープをコンポーネントサブツリー内に限定できる
  • 状態の重複や競合を避けられる

実装ステップ

1. Zustandストアの作成(vanilla モード)

// store.ts
import { createStore } from 'zustand/vanilla';

const createBearStore = (initialBears: number) =>
  createStore((set) => ({
    bears: initialBears,
    increasePopulation: (by: number) =>
      set((state) => ({ bears: state.bears + by })),
    removeAllBears: () => set({ bears: 0 }),
  }));

export default createBearStore;

2. React Contextの作成

// context.ts
import React from 'react';
import type { StoreApi } from 'zustand';
import type { BearState } from './types';

export const BearStoreContext = React.createContext<StoreApi<BearState> | null>(null);

3. Providerコンポーネントでストアを生成・提供

// BearStoreProvider.tsx
import { useMemo } from 'react';
import createBearStore from './store';
import { BearStoreContext } from './context';

export const BearStoreProvider = ({
  children,
  initialBears = 0,
}: {
  children: React.ReactNode;
  initialBears: number;
}) => {
  const store = useMemo(() => createBearStore(initialBears), [initialBears]);

  return (
    <BearStoreContext.Provider value={store}>
      {children}
    </BearStoreContext.Provider>
  );
};

4. カスタムフックでストアを利用

// useBearStore.ts
import { useContext } from 'react';
import { useStore } from 'zustand';
import { BearStoreContext } from './context';

export const useBearStore = <T>(selector: (state: BearState) => T): T => {
  const store = useContext(BearStoreContext);
  if (!store) {
    throw new Error('BearStoreProviderでラップされていません');
  }
  return useStore(store, selector);
};

5. コンポーネントで使用

// BearCounter.tsx
import { useBearStore } from './useBearStore';

export const BearCounter = () => {
  const bears = useBearStore((state) => state.bears);
  const increase = useBearStore((state) => state.increasePopulation);

  return (
    <div>
      <h2>{bears} bears</h2>
      <button onClick={() => increase(1)}>増やす</button>
    </div>
  );
};

このパターンのメリット

  • ✅ コンポーネントツリー単位で状態をスコープできる
  • ✅ 複数インスタンスを独立して扱える
  • ✅ テストが容易(propsから状態を与えられる)
  • ✅ グローバルストアとの干渉がない

このように、Zustandの「vanillaストア」とReact Contextをうまく組み合わせることで、アプリケーション内での状態の使い分けがしやすくなる。


まとめ

  • Reactにおける状態管理は「どこで、どのくらいのスコープで共有すべきか」が非常に重要である。
  • 全てをグローバルにすると柔軟性を失い、逆に局所的すぎると状態の再利用性や一貫性が損なわれる。
  • ZustandとReact Contextの併用により、「必要なところにだけ状態を届ける」設計が可能となる。
  • このパターンは、アプリケーションの成長に応じて状態管理をスケーラブルにするうえで非常に有効である。

出典・参考

Discussion

ログインするとコメントできます