Zustand入門 - 軽量なReact状態管理を実装する

初級 | 35分 で読める | 2025.12.08

公式ドキュメント

今回やること

Zustandは、Reactでグローバル状態を小さく扱うための状態管理ライブラリです。

便利だからといって全ての状態をZustandに入れるのではなく、共有が必要な状態だけをストアに置くことが重要です。

この記事では次を学びます。

  • Zustandの基本
  • ストア作成
  • アクション定義
  • セレクター
  • ミドルウェア

Step 1: セットアップ

npm install zustand

ポイント: ZustandはProviderが不要で、createで作ったフックをどこからでも呼び出せます。Reduxと異なりボイラープレートが最小限です。

Step 2: 基本的なストア

// store/counterStore.ts
import { create } from 'zustand';

interface CounterState {
 count: number;
 increment: () => void;
 decrement: () => void;
 reset: () => void;
}

export const useCounterStore = create<CounterState>((set) => ({
 count: 0,
 increment: () => set((state) => ({ count: state.count + 1 })),
 decrement: () => set((state) => ({ count: state.count - 1 })),
 reset: () => set({ count: 0 }),
}));

Step 3: コンポーネントで使用

function Counter() {
 const { count, increment, decrement, reset } = useCounterStore();

 return (
 <div>
 <p>Count: {count}</p>
 <button onClick={increment}>+</button>
 <button onClick={decrement}>-</button>
 <button onClick={reset}>Reset</button>
 </div>
 );
}

実践メモ: セレクターを使うと、ストアの一部だけを購読できます。必要な状態のみを取得することで不要な再レンダリングを防げます。

Step 4: セレクターで最適化

// 必要な値のみ購読
function CountDisplay() {
 const count = useCounterStore((state) => state.count);
 return <p>Count: {count}</p>;
}

function CountActions() {
 const increment = useCounterStore((state) => state.increment);
 return <button onClick={increment}>+</button>;
}

Step 5: 複雑なストア

// store/todoStore.ts
import { create } from 'zustand';

interface Todo {
 id: number;
 text: string;
 completed: boolean;
}

interface TodoState {
 todos: Todo[];
 addTodo: (text: string) => void;
 toggleTodo: (id: number) => void;
 removeTodo: (id: number) => void;
}

export const useTodoStore = create<TodoState>((set) => ({
 todos: [],
 addTodo: (text) =>
 set((state) => ({
 todos: [...state.todos, { id: Date.now(), text, completed: false }],
 })),
 toggleTodo: (id) =>
 set((state) => ({
 todos: state.todos.map((todo) =>
 todo.id === id ? { ...todo, completed: !todo.completed } : todo
 ),
 })),
 removeTodo: (id) =>
 set((state) => ({
 todos: state.todos.filter((todo) => todo.id !== id),
 })),
}));

Step 6: 永続化ミドルウェア

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

export const useSettingsStore = create(
 persist<SettingsState>(
 (set) => ({
 theme: 'light',
 setTheme: (theme) => set({ theme }),
 }),
 {
 name: 'settings-storage',
 }
 )
);

Step 7: 非同期アクション

interface UserState {
 user: User | null;
 loading: boolean;
 fetchUser: (id: string) => Promise<void>;
}

export const useUserStore = create<UserState>((set) => ({
 user: null,
 loading: false,
 fetchUser: async (id) => {
 set({ loading: true });
 try {
 const res = await fetch(`/api/users/${id}`);
 const user = await res.json();
 set({ user, loading: false });
 } catch {
 set({ loading: false });
 }
 },
}));

状態管理を増やしすぎない

Zustandは軽量ですが、すべての状態をグローバルストアに入れると逆に追いにくくなります。フォームの入力途中やモーダルの開閉など、画面内で完結する状態はReactのローカルstateで十分な場合があります。

グローバルに置くべきなのは、複数画面で共有するユーザー情報、テーマ設定、カート、通知などです。状態の置き場所を決めるときは「ページを移動しても必要か」「複数の離れたコンポーネントが読むか」を基準にすると判断しやすくなります。

設計の目安

  • サーバーデータはTanStack Queryなどに任せる
  • UIだけの一時状態はローカルstateに置く
  • 永続化する値はバージョン変更時の移行も考える
  • action名は何が起きるか分かる動詞にする

動作確認

Zustandのストアを作ったら、次を確認します。

確認項目見ること
初期値画面初回表示で期待した値になる
actionボタン操作で状態が変わる
selector不要な再レンダリングが増えていない
persistリロード後も保存値が残る
非同期処理loadingとerrorを表現できる

状態が更新されない時は、オブジェクトを直接変更していないか、action内で新しい配列やオブジェクトを返しているか確認します。

練習

Todoストアで次を実装してください。

  1. todos の一覧を持つ
  2. addTodo でTodoを追加する
  3. toggleTodo で完了状態を切り替える
  4. 未完了件数をselectorで取得する
  5. persist でリロード後もTodoを残す

次のステップ

サーバーデータの管理はZustandではなく TanStack Query入門SWRによるデータフェッチング が向いています。React Hooksの基礎は React Hooks完全マスター も確認してください。

まとめ

注意: persistミドルウェアでローカルストレージに保存する際、シリアライズできないデータ(関数・Map等)は除外設定が必要です

ZustandはReduxより軽量でシンプルな状態管理ライブラリです。Providerが不要で、セレクターによる最適化も簡単です。

参考リソース

← 一覧に戻る
PR
PR
PR
PR