Zustandとは?
Zustandは、Reduxのような定型的なコードを必要とせず、Reactのフックに基づいたシンプルなAPIでグローバル状態を管理できるライブラリです。
- ミニマルなAPI:
create
関数ひとつで、状態(state)とそれを更新するアクション(action)を持つ「Store」を定義できます。 - Hookベース: コンポーネント内では、カスタムフックを呼び出すだけで状態にアクセスできます。
- パフォーマンス: 必要なデータだけをセレクターで選択するため、関係のない状態更新による不要な再レンダリングを防ぎます。
- Context Provider不要: アプリケーションのルートを
<Provider>
でラップする必要がありません。
インストール
npm install zustand
# or
yarn add zustand
基本的な使い方
1. Storeの作成
create
関数を使い、状態とアクションを定義します。set
関数が状態を更新する役割を担います。
// stores/useCounterStore.ts
import { create } from 'zustand';
interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
}
export const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
2. コンポーネントでの利用
作成したStoreは、コンポーネント内でフックとして呼び出すだけで利用できます。
import { useCounterStore } from '@/stores/useCounterStore';
const CounterComponent = () => {
const { count, increment, decrement } = useCounterStore();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
</div>
);
};
3. セレクターによる最適化
Storeから必要な状態のみを抽出することで、不要な再レンダリングを防ぐことができます。
// count のみが変更されたときだけ再レンダリングされる
const count = useCounterStore((state) => state.count);
非同期アクション
async/await
を使い、Store内で直接非同期処理を記述できます。
import { create } from 'zustand';
interface UserState {
users: { id: number; name: string }[];
fetchUsers: () => Promise<void>;
}
export const useUserStore = create<UserState>((set) => ({
users: [],
fetchUsers: async () => {
try {
const response = await fetch('/api/users');
const users = await response.json();
set({ users });
} catch (error) {
console.error('Failed to fetch users', error);
}
},
}));
ミドルウェアの活用
Zustandは、機能を拡張するためのミドルウェアがあります。
Immer
immer
ミドルウェアを使うと、ネストした複雑な状態を安全に更新できます。
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
// set((state) => { state.deep.nested.value = 'new'; }); のようにミューテーションが可能になる
export const useImmerStore = create<MyState>()(
immer((set) => ({
// ...
}))
);
Persist
persist
ミドルウェアは、Storeの状態をlocalStorage
などのストレージに自動的に保存・復元します。
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}),
{
name: 'settings-storage', // ストレージのキー名
}
)
);
DevTools
devtools
ミドルウェアを導入すると、Redux DevTools拡張機能で状態の変更履歴を追跡・デバッグできます。
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
export const useDevToolsStore = create<MyState>()(
devtools(
(set) => ({
// ...
}),
{
name: 'my-store', // DevToolsでの表示名
}
)
);
ミドルウェアは、devtools(persist(immer(...)))
のように組み合わせて利用することも可能です。
Storeのテスト
ZustandのStoreはReactから独立しているため、@testing-library/react
のrenderHook
を使って簡単にテストできます。
import { renderHook, act } from '@testing-library/react';
import { useCounterStore } from '@/stores/useCounterStore';
describe('useCounterStore', () => {
// 各テストの前に状態をリセット
beforeEach(() => {
useCounterStore.setState({ count: 0 });
});
it('should increment count', () => {
const { result } = renderHook(() => useCounterStore());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
});
まとめ
Zustandは、簡単なだけでなく、ミドルウェアによる拡張性やパフォーマンスも十分で、プロジェクトの規模を問わずReactの状態管理の有力な選択肢かなとおもいます。