mcpField
Z
zustand-state-management

zustand-state-management

github.com/jezweb開発1.1k1.2k 回インストール3 週間前に更新
リポジトリ

zustand-state-management について

React と TypeScript の開発者向けに、Zustand の状態管理を解説するガイドです。ストア作成、セレクター、ミドルウェアに加え、永続化、Next.js のハイドレーション、非同期アクションのパターンとテンプレートを収録しています。

主な機能

  1. 01JavaScript と TypeScript のストアパターン
  2. 02セレクターと状態更新の指針
  3. 03永続化と Devtools ミドルウェア
  4. 04Next.js のハイドレーション問題への対処
  5. 05非同期アクションと派生値のパターン
  6. 06移行ガイドとコードテンプレート

活用シーン

  1. 01React アプリの状態ストア設計
  2. 02状態の永続化と Devtools の設定
  3. 03Next.js のハイドレーション不一致への対処
  4. 04セレクターが原因の再レンダーの調査

インストール

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

$npx skills add https://github.com/jezweb/claude-skills --skill zustand-state-management

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

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

README.md
.claude-plugin/
plugin.json
assets/
example-template.txt
references/
example-reference.md
middleware-guide.md
migration-guide.md
nextjs-hydration.md
typescript-patterns.md
rules/
zustand-state-management.md
scripts/
check-versions.sh
example-script.sh
templates/
async-actions-store.ts
basic-store.ts
computed-store.ts
devtools-store.ts
nextjs-store.ts
persist-store.ts
slices-pattern.ts
typescript-store.ts

詳細

Zustand の状態管理

最終更新日:2026-01-21 最新バージョン:[email protected](リリース日:2026-01-12) 依存関係:React 18-19、TypeScript 5+


クイックスタート

npm install zustand

TypeScript ストア(重要:create<T>()() の二重括弧を使用します):

import { create } from 'zustand'

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

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

コンポーネントでの使用:

const bears = useBearStore((state) => state.bears)  // Only re-renders when bears changes
const increase = useBearStore((state) => state.increase)

基本パターン

基本的なストア(JavaScript):

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

TypeScript ストア(推奨):

interface CounterStore { count: number; increment: () => void }
const useStore = create<CounterStore>()((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}))

永続化ストア(ページを再読み込みしても状態が保持されます):

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

const useStore = create<UserPreferences>()(
  persist(
    (set) => ({ theme: 'system', setTheme: (theme) => set({ theme }) }),
    { name: 'user-preferences', storage: createJSONStorage(() => localStorage) },
  ),
)

重要なルール

必ず行うこと

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

絶対に行わないこと

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


既知の問題の予防

このスキルは、文書化された 6 件の問題を防止します:

問題 #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.9"
問題 #4:無限レンダーループ

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

Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate.

出典:

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

v5 の破壊的変更:Zustand v5 では、v4 と比べてこのエラーがより明確に示されるようになりました。v4 では、この動作は「理想的ではない」ものの、見過ごされることがありました。v5 では、すぐに「Maximum update depth exceeded」エラーが表示されます。

予防策:

import { useShallow } 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 useShallow hook for multiple values
const { bears, fishes } = useStore(
  useShallow((state) => ({ bears: state.bears, fishes: state.fishes }))
)
問題 #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),
}))
Issue #6:Persist ミドルウェアの競合状態(v5.0.10+ で修正済み)

エラー:リエ水和の同時試行中に状態が一致しない

ソース:

発生する理由: Zustand v5.0.9 以前では、persist ミドルウェアの初期化中にリエ水和が同時に呼び出されると、複数の水和処理が互いに干渉する競合状態が発生し、状態が一致しなくなることがありました。

防止策: Zustand v5.0.10 以降にアップグレードしてください。コードの変更は不要です。修正は persist ミドルウェアに内部的に適用されています。

npm install zustand@latest  # Ensure v5.0.10+

注:この問題は v5.0.10 で修正されました(2026 年 1 月)。v5.0.9 以前を使用していて、persist ミドルウェアで状態の不整合が発生している場合は、すぐにアップグレードしてください。


ミドルウェア

Persist(localStorage):

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

const useStore = create<MyStore>()(
  persist(
    (set) => ({ data: [], addItem: (item) => set((state) => ({ data: [...state.data, item] })) }),
    {
      name: 'my-storage',
      partialize: (state) => ({ data: state.data }),  // Only persist 'data'
    },
  ),
)

Devtools(Redux DevTools):

import { devtools } from 'zustand/middleware'

const useStore = create<CounterStore>()(
  devtools(
    (set) => ({ count: 0, increment: () => set((s) => ({ count: s.count + 1 }), undefined, 'increment') }),
    { name: 'CounterStore' },
  ),
)

v4→v5 移行に関する注記:Zustand v4 では、devtools は 'zustand/middleware/devtools' からインポートしていました。v5 では、(上記のように)'zustand/middleware' を使用してください。「Module not found: Can't resolve 'zustand/middleware/devtools'」というエラーが表示される場合は、インポートパスを更新してください。

ミドルウェアの組み合わせ(順序が重要です):

const useStore = create<MyStore>()(devtools(persist((set) => ({ /* ... */ }), { name: 'storage' }), { name: 'MyStore' }))

よく使われるパターン

計算値/派生値(保存せず、セレクター内で計算):

const count = useStore((state) => state.items.length)  // Computed on read

非同期アクション:

const useAsyncStore = create<AsyncStore>()((set) => ({
  data: null,
  isLoading: false,
  fetchData: async () => {
    set({ isLoading: true })
    const response = await fetch('/api/data')
    set({ data: await response.text(), isLoading: false })
  },
}))

ストアのリセット:

const initialState = { count: 0, name: '' }
const useStore = create<ResettableStore>()((set) => ({
  ...initialState,
  reset: () => set(initialState),
}))

パラメーター付きセレクター:

const todo = useStore((state) => state.todos.find((t) => t.id === id))

バンドルされたリソース

テンプレート:basic-store.ts、typescript-store.ts、persist-store.ts、slices-pattern.ts、devtools-store.ts、nextjs-store.ts、computed-store.ts、async-actions-store.ts

リファレンス:middleware-guide.md(persist/devtools/immer/custom)、typescript-patterns.md(型推論の問題)、nextjs-hydration.md(SSR/水和)、migration-guide.md(Redux/Context/v4 からの移行)

スクリプト:check-versions.sh(バージョン互換性)


高度なトピック

Vanilla ストア(React なし):

import { createStore } from 'zustand/vanilla'

const store = createStore<CounterStore>()((set) => ({ count: 0, increment: () => set((s) => ({ count: s.count + 1 })) }))
const unsubscribe = store.subscribe((state) => console.log(state.count))
store.getState().increment()

カスタムミドルウェア:

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

Immer ミドルウェア(ミュータブルな更新):

import { immer } from 'zustand/middleware/immer'

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

v5.0.3→v5.0.4 移行に関する注記:v5.0.3 から v5.0.4 以降にアップグレードした後、immer ミドルウェアが動作しなくなった場合は、上記のインポートパス(zustand/middleware/immer)を使用していることを確認してください。v5.0.4 への更新後に問題が発生したという報告があり、正しいインポートパスを確認することで解決しています。

実験的な SSR セーフミドルウェア(v5.0.9+):

ステータス:実験的(API は変更される可能性があります)

Zustand v5.0.9 では、Next.js での使用向けに実験的な unstable_ssrSafe ミドルウェアが導入されました。これは、_hasHydrated パターン(Issue #1 を参照)に代わる方法を提供します。

import { unstable_ssrSafe } from 'zustand/middleware'

const useStore = create<Store>()(
  unstable_ssrSafe(
    persist(
      (set) => ({ /* state */ }),
      { name: 'my-store' }
    )
  )
)

推奨:この API が安定するまでは、Issue #1 で説明されている _hasHydrated パターンを引き続き使用してください。安定版になる時期については、Discussion #2740 で最新情報を確認してください。


公式ドキュメント

コメント