🐻
特定の Component tree だけで使える状態管理をZustand + React Contextで実現する
ご注意!
この記事は私が知りたい内容を 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