mcpField
Z
zustand-state-management

zustand-state-management

github.com/jackspace開発1665 回インストール13 週間前に更新
リポジトリ

zustand-state-management について

React プロジェクトで Zustand を使うための Claude スキルです。TypeScript、永続化、ミドルウェア、Next.js のハイドレーションを解説し、テンプレートとトラブルシューティングも含みます。

主な機能

  1. 01型付き・基本的なストアパターン
  2. 02セレクターと不変更新
  3. 03永続化とDevToolsミドルウェア
  4. 04Next.jsのハイドレーションガイド
  5. 05テンプレートとトラブルシューティング

活用シーン

  1. 01ReactアプリでZustandストアを作成
  2. 02再読み込み後も設定を保持
  3. 03TypeScriptや再レンダリングループの問題を調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jackspace/claudeskillz --skill zustand-state-management

インストール後、Claude Code で/zustand-state-managementを入力して読み込まれたか確認します。

同梱ファイルREADME.mdSKILL.json

詳細

Zustand による状態管理

ステータス: 本番環境で使用可能 ✅ 最終更新日: 2025-10-24 最新バージョン: [email protected] 依存関係: React 18+、TypeScript 5+


クイックスタート(3 分)

1. Zustand をインストールする
npm install zustand
# or
pnpm add zustand
# or
yarn add zustand

Zustand を選ぶ理由

  • 最小限の API:覚える必要がある関数は 1 つだけです(create)
  • ボイラープレート不要:プロバイダー、リデューサー、アクションは不要です
  • TypeScript を第一に設計:優れた型推論に対応しています
  • 高速:粒度の細かいサブスクリプションにより、不要な再レンダリングを防ぎます
  • 柔軟:永続化、開発者ツールなどのミドルウェアを利用できます
2. 最初のストアを作成する(TypeScript)
import { create } from 'zustand'

interface BearStore {
  bears: number
  increase: (by: number) => void
  reset: () => void
}

const useBearStore = create<BearStore>()((set) => ({
  bears: 0,
  increase: (by) => set((state) => ({ bears: state.bears + by })),
  reset: () => set({ bears: 0 }),
}))

重要: create<T>()() の二重の丸括弧に注目してください。TypeScript でミドルウェアを使うには、これが必要です。

3. コンポーネントでストアを使用する
import { useBearStore } from './store'

function BearCounter() {
  const bears = useBearStore((state) => state.bears)
  return <h1>{bears} around here...</h1>
}

function Controls() {
  const increase = useBearStore((state) => state.increase)
  return <button onClick={() => increase(1)}>Add bear</button>
}

これが機能する理由

  • 選択した状態が変化した場合にのみ、コンポーネントが再レンダリングされます
  • Context プロバイダーは必要ありません
  • セレクター関数で状態の特定の部分を取り出します

3 つのパターンによるセットアップ手順

パターン 1:基本的なストア(JavaScript)

TypeScript を使わないシンプルな用途向けです。

import { create } from 'zustand'

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}))

使用するケース

  • プロトタイピング
  • 小規模なアプリ
  • プロジェクトで TypeScript を使わない場合
パターン 2:TypeScript ストア(推奨)

型安全性が必要な本番環境のアプリ向けです。

import { create } from 'zustand'

// Define store interface
interface CounterStore {
  count: number
  increment: () => void
  decrement: () => void
}

// Create typed store
const useCounterStore = create<CounterStore>()((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}))

重要なポイント

  • 状態とアクションには、それぞれ個別のインターフェースを定義します
  • create<T>()() 構文を使います(ミドルウェア用のカリー化)
  • IDE のオートコンプリートと型チェックをすべて利用できます
パターン 3:永続化ストア

ページを再読み込みしても状態を保持する場合に使います。

import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'

interface UserPreferences {
  theme: 'light' | 'dark' | 'system'
  language: string
  setTheme: (theme: UserPreferences['theme']) => void
  setLanguage: (language: string) => void
}

const usePreferencesStore = create<UserPreferences>()(
  persist(
    (set) => ({
      theme: 'system',
      language: 'en',
      setTheme: (theme) => set({ theme }),
      setLanguage: (language) => set({ language }),
    }),
    {
      name: 'user-preferences', // unique name in localStorage
      storage: createJSONStorage(() => localStorage), // optional: defaults to localStorage
    },
  ),
)

これが重要な理由

  • 状態は localStorage に自動的に保存されます
  • ページの再読み込み時に復元されます
  • sessionStorage にも対応しています
  • シリアライズは自動的に処理されます

重要なルール

必ず行うこと

✅ TypeScript でミドルウェアとの互換性を確保するため、create<T>()()(二重の丸括弧)を使う
✅ 状態とアクションには、それぞれ個別のインターフェースを定義する
✅ セレクター関数を使って、状態の特定の部分を取り出す
✅ 派生状態には、更新関数を使って set を呼び出す:set((state) => ({ count: state.count + 1 }))
✅ persist ミドルウェアのストレージキーには、一意の名前を使う
✅ hasHydrated フラグのパターンを使って、Next.js のハイドレーションを処理する
✅ 複数の値を選択する場合は shallow を使う
✅ アクションは純粋に保つ(状態の更新以外の副作用を発生させない)

絶対に行わないこと

❌ TypeScript で create<T>(...)(単一の丸括弧)を使う。ミドルウェアの型が壊れます
❌ 状態を直接変更する:set((state) => { state.count++; return state })。代わりに不変性を保った更新を行います
❌ セレクター内で新しいオブジェクトを作成する:useStore((state) => ({ a: state.a }))。無限に再レンダリングされます
❌ 複数のストアで同じストレージ名を使う。データが競合します
❌ ハイドレーションを確認せずに SSR 中に localStorage にアクセスする
❌ サーバー状態に Zustand を使う。代わりに TanStack Query を使います
❌ ストアのインスタンスを直接エクスポートする。必ずフックをエクスポートします


既知の問題の防止

このスキルは、文書化された 5 件の問題を防ぎます。

問題 #1:Next.js のハイドレーション不一致

エラー: "Text content does not match server-rendered HTML" または "Hydration failed"

出典:

発生する理由: persist ミドルウェアはクライアント上では localStorage から読み込みますが、サーバー上では読み込まないため、状態が一致しなくなります。

防止策:

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

interface StoreWithHydration {
  count: number
  _hasHydrated: boolean
  setHasHydrated: (hydrated: boolean) => void
  increase: () => void
}

const useStore = create<StoreWithHydration>()(
  persist(
    (set) => ({
      count: 0,
      _hasHydrated: false,
      setHasHydrated: (hydrated) => set({ _hasHydrated: hydrated }),
      increase: () => set((state) => ({ count: state.count + 1 })),
    }),
    {
      name: 'my-store',
      onRehydrateStorage: () => (state) => {
        state?.setHasHydrated(true)
      },
    },
  ),
)

// In component
function MyComponent() {
  const hasHydrated = useStore((state) => state._hasHydrated)

  if (!hasHydrated) {
    return <div>Loading...</div>
  }

  // Now safe to render with persisted state
  return <ActualContent />
}
問題 #2:TypeScript で二重の丸括弧が不足

エラー: 型推論に失敗し、ミドルウェアを使うと StateCreator の型が壊れます

出典: 公式 Zustand TypeScript ガイド

発生する理由: ミドルウェアを TypeScript の型推論とともに動作させるには、カリー化構文 create<T>()() が必要です。

防止策:

// ❌ WRONG - Single parentheses
const useStore = create<MyStore>((set) => ({
  // ...
}))

// ✅ CORRECT - Double parentheses
const useStore = create<MyStore>()((set) => ({
  // ...
}))

ルール: ミドルウェアを使わない場合でも、将来に備えて TypeScript では常に create<T>()() を使ってください。

問題 #3:persist ミドルウェアのインポートエラー

エラー: "Attempted import error: 'createJSONStorage' is not exported from 'zustand/middleware'"

出典: GitHub Discussion #2839

発生する理由: インポートパスが誤っているか、zustand とビルドツールのバージョンが一致していません。

防止策:

// ✅ CORRECT imports for v5
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'

// Verify versions
// [email protected] includes createJSONStorage
// [email protected] uses different API

// Check your package.json
// "zustand": "^5.0.8"
Issue #4:無限レンダリングループ

エラー:コンポーネントが無限に再レンダリングされ、ブラウザーがフリーズします

出典:GitHub Discussions #2642

発生する理由: セレクターで新しいオブジェクト参照を作成すると、Zustand は状態が変更されたと判断します。

防止方法:

import { shallow } from 'zustand/shallow'

// ❌ WRONG - Creates new object every time
const { bears, fishes } = useStore((state) => ({
  bears: state.bears,
  fishes: state.fishes,
}))

// ✅ CORRECT Option 1 - Select primitives separately
const bears = useStore((state) => state.bears)
const fishes = useStore((state) => state.fishes)

// ✅ CORRECT Option 2 - Use shallow for multiple values
const { bears, fishes } = useStore(
  (state) => ({ bears: state.bears, fishes: state.fishes }),
  shallow,
)
Issue #5:Slices パターンにおける TypeScript の複雑さ

エラー:StateCreator の型推論に失敗し、複雑なミドルウェアの型が機能しません

出典:公式 Slices パターンガイド

発生する理由: 複数のスライスを組み合わせるには、ミドルウェアとの互換性を保つために明示的な型注釈が必要です。

防止方法:

import { create, StateCreator } from 'zustand'

// Define slice types
interface BearSlice {
  bears: number
  addBear: () => void
}

interface FishSlice {
  fishes: number
  addFish: () => void
}

// Create slices with proper types
const createBearSlice: StateCreator<
  BearSlice & FishSlice,  // Combined store type
  [],                      // Middleware mutators (empty if none)
  [],                      // Chained middleware (empty if none)
  BearSlice               // This slice's type
> = (set) => ({
  bears: 0,
  addBear: () => set((state) => ({ bears: state.bears + 1 })),
})

const createFishSlice: StateCreator<
  BearSlice & FishSlice,
  [],
  [],
  FishSlice
> = (set) => ({
  fishes: 0,
  addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
})

// Combine slices
const useStore = create<BearSlice & FishSlice>()((...a) => ({
  ...createBearSlice(...a),
  ...createFishSlice(...a),
}))

ミドルウェアの設定

Persist ミドルウェア(localStorage)
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'

interface MyStore {
  data: string[]
  addItem: (item: string) => void
}

const useStore = create<MyStore>()(
  persist(
    (set) => ({
      data: [],
      addItem: (item) => set((state) => ({ data: [...state.data, item] })),
    }),
    {
      name: 'my-storage',
      storage: createJSONStorage(() => localStorage),
      partialize: (state) => ({ data: state.data }), // Only persist 'data'
    },
  ),
)
Devtools ミドルウェア(Redux DevTools)
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'

interface CounterStore {
  count: number
  increment: () => void
}

const useStore = create<CounterStore>()(
  devtools(
    (set) => ({
      count: 0,
      increment: () =>
        set(
          (state) => ({ count: state.count + 1 }),
          undefined,
          'counter/increment', // Action name in DevTools
        ),
    }),
    { name: 'CounterStore' }, // Store name in DevTools
  ),
)
複数のミドルウェアを組み合わせる
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'

const useStore = create<MyStore>()(
  devtools(
    persist(
      (set) => ({
        // store definition
      }),
      { name: 'my-storage' },
    ),
    { name: 'MyStore' },
  ),
)

順序が重要です:devtools(persist(...)) を使用すると、DevTools に persist アクションが表示されます。


よく使われるパターン

パターン:計算値/派生値
interface StoreWithComputed {
  items: string[]
  addItem: (item: string) => void
  // Computed in selector, not stored
}

const useStore = create<StoreWithComputed>()((set) => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
}))

// Use in component
function ItemCount() {
  const count = useStore((state) => state.items.length)
  return <div>{count} items</div>
}
パターン:非同期アクション
interface AsyncStore {
  data: string | null
  isLoading: boolean
  error: string | null
  fetchData: () => Promise<void>
}

const useAsyncStore = create<AsyncStore>()((set) => ({
  data: null,
  isLoading: false,
  error: null,
  fetchData: async () => {
    set({ isLoading: true, error: null })
    try {
      const response = await fetch('/api/data')
      const data = await response.text()
      set({ data, isLoading: false })
    } catch (error) {
      set({ error: (error as Error).message, isLoading: false })
    }
  },
}))
パターン:ストアのリセット
interface ResettableStore {
  count: number
  name: string
  increment: () => void
  reset: () => void
}

const initialState = {
  count: 0,
  name: '',
}

const useStore = create<ResettableStore>()((set) => ({
  ...initialState,
  increment: () => set((state) => ({ count: state.count + 1 })),
  reset: () => set(initialState),
}))
パターン:パラメーター付きセレクター
interface TodoStore {
  todos: Array<{ id: string; text: string; done: boolean }>
  addTodo: (text: string) => void
  toggleTodo: (id: string) => void
}

const useStore = create<TodoStore>()((set) => ({
  todos: [],
  addTodo: (text) =>
    set((state) => ({
      todos: [...state.todos, { id: Date.now().toString(), text, done: false }],
    })),
  toggleTodo: (id) =>
    set((state) => ({
      todos: state.todos.map((todo) =>
        todo.id === id ? { ...todo, done: !todo.done } : todo
      ),
    })),
}))

// Use with parameter
function Todo({ id }: { id: string }) {
  const todo = useStore((state) => state.todos.find((t) => t.id === id))
  const toggleTodo = useStore((state) => state.toggleTodo)

  if (!todo) return null

  return (
    <div>
      <input
        type="checkbox"
        checked={todo.done}
        onChange={() => toggleTodo(id)}
      />
      {todo.text}
    </div>
  )
}

バンドルされているリソースの使用

テンプレート(templates/)

このスキルには、すぐに使えるテンプレートファイルが8個含まれています。

  • basic-store.ts - 最小限の JavaScript ストアの例
  • typescript-store.ts - 適切に型付けされた TypeScript ストア
  • persist-store.ts - マイグレーションに対応した localStorage への永続化
  • slices-pattern.ts - モジュール式のストア構成
  • devtools-store.ts - Redux DevTools との統合
  • nextjs-store.ts - ハイドレーションに対応した SSR セーフな Next.js ストア
  • computed-store.ts - 派生状態のパターン
  • async-actions-store.ts - ローディング状態に対応した非同期処理

使用例:

# Copy template to your project
cp ~/.claude/skills/zustand-state-management/templates/typescript-store.ts src/store/

それぞれの使用場面:

  • 手早くプロトタイプを作成する場合は basic-store.ts を使用します
  • ほとんどの本番アプリでは typescript-store.ts を使用します
  • ページの再読み込み後も状態を保持する必要がある場合は persist-store.ts を使用します
  • 大規模で複雑なストア(100行以上)では slices-pattern.ts を使用します
  • SSR を使用する Next.js プロジェクトでは nextjs-store.ts を使用します
リファレンス(references/)

複雑なケースに対応する詳細なドキュメント:

  • middleware-guide.md - ミドルウェアの完全なドキュメント(persist、devtools、immer、カスタム)
  • typescript-patterns.md - 高度な TypeScript パターンとトラブルシューティング
  • nextjs-hydration.md - SSR、ハイドレーション、Next.js のベストプラクティス
  • migration-guide.md - Redux、Context API、または Zustand v4 からの移行

Claude がこれらを読み込む場面:

  • ユーザーが永続化、devtools、またはカスタムミドルウェアについて尋ねた場合は middleware-guide.md を読み込みます
  • 複雑な型推論の問題が発生した場合は typescript-patterns.md を読み込みます
  • Next.js 固有の問題には nextjs-hydration.md を読み込みます
  • ほかの状態管理ソリューションから移行する場合は migration-guide.md を読み込みます
スクリプト(scripts/)
  • check-versions.sh - Zustand のバージョンと互換性を確認します

使用方法:

cd your-project/
~/.claude/skills/zustand-state-management/scripts/check-versions.sh

高度なトピック

Vanilla ストア(React なし)
import { createStore } from 'zustand/vanilla'

const store = createStore<CounterStore>()((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}))

// Subscribe to changes
const unsubscribe = store.subscribe((state) => {
  console.log('Count changed:', state.count)
})

// Get current state
console.log(store.getState().count)

// Update state
store.getState().increment()

// Cleanup
unsubscribe()
カスタムミドルウェア
import { StateCreator, StoreMutatorIdentifier } from 'zustand'

type Logger = <T>(
  f: StateCreator<T, [], []>,
  name?: string,
) => StateCreator<T, [], []>

const logger: Logger = (f, name) => (set, get, store) => {
  const loggedSet: typeof set = (...a) => {
    set(...(a as Parameters<typeof set>))
    console.log(`[${name}]:`, get())
  }
  return f(loggedSet, get, store)
}

// Use custom middleware
const useStore = create<MyStore>()(
  logger((set) => ({
    // store definition
  }), 'MyStore'),
)
Immer ミドルウェア(可変更新)
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'

interface TodoStore {
  todos: Array<{ id: string; text: string }>
  addTodo: (text: string) => void
}

const useStore = create<TodoStore>()(
  immer((set) => ({
    todos: [],
    addTodo: (text) =>
      set((state) => {
        // Mutate directly with Immer
        state.todos.push({ id: Date.now().toString(), text })
      }),
  })),
)

依存関係

必須:

任意:

  • @types/node - TypeScript のパス解決用
  • immer - 可変更新構文用
  • Redux DevTools Extension - devtools ミドルウェア用

公式ドキュメント


パッケージのバージョン(2025-10-24 確認済み)

{
  "dependencies": {
    "zustand": "^5.0.8",
    "react": "^19.0.0"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "typescript": "^5.0.0"
  }
}

互換性:

  • React 18+、React 19 ✅
  • TypeScript 5+ ✅
  • Next.js 14+、Next.js 15+ ✅
  • Vite 5+ ✅

トラブルシューティング

問題:ストアの更新によって再レンダリングが発生しない

解決策:分割代入ではなく、セレクター関数を使用してください。const bears = useStore(state => state.bears) のように記述します。const { bears } = useStore() ではありません。

問題:ミドルウェアで TypeScript エラーが発生する

解決策:二重の括弧を使用してください。create<T>()() であり、create<T>() ではありません。

問題:Persist ミドルウェアによってハイドレーションエラーが発生する

解決策:_hasHydrated フラグのパターンを実装してください(Issue #1 を参照)。

問題:Redux DevTools にアクションが表示されない

解決策:set の第 3 引数としてアクション名を渡してください。set(newState, undefined, 'actionName')

問題:ストアの状態が予期せずリセットされる

解決策:HMR(ホットモジュールリプレースメント)を使用しているか確認してください。Zustand は開発中、モジュールの再読み込み時にリセットされます。


セットアップの完全チェックリスト

このチェックリストを使って、Zustand のセットアップを確認してください。

  • [email protected] 以降をインストールした
  • 適切な TypeScript 型を指定してストアを作成した
  • create<T>()() の二重括弧構文を使用した
  • コンポーネント内でセレクター関数をテストした
  • 選択した状態が変化したときにのみ、コンポーネントが再レンダリングされることを確認した
  • persist を使用する場合:一意のストレージ名を設定した
  • persist を使用する場合:Next.js 用にハイドレーションチェックを実装した
  • devtools を使用する場合:デバッグ用にアクションに名前を付けた
  • slices を使用する場合:各スライスの StateCreator に適切な型を指定した
  • すべてのアクションが純粋関数である
  • 状態を直接変更していない
  • ストアが本番ビルドで動作する

質問や問題がありますか?

  1. TypeScript に関するヘルプは references/typescript-patterns.md を確認してください
  2. Next.js に関する問題は references/nextjs-hydration.md を確認してください
  3. persist/devtools に関するヘルプは references/middleware-guide.md を確認してください
  4. 公式ドキュメント:https://zustand.docs.pmnd.rs/
  5. GitHub の問題:https://github.com/pmndrs/zustand/issues

コメント