zustand-state-management について
React と TypeScript の開発者向けに、Zustand の状態管理を解説するガイドです。ストア作成、セレクター、ミドルウェアに加え、永続化、Next.js のハイドレーション、非同期アクションのパターンとテンプレートを収録しています。
主な機能
- 01JavaScript と TypeScript のストアパターン
- 02セレクターと状態更新の指針
- 03永続化と Devtools ミドルウェア
- 04Next.js のハイドレーション問題への対処
- 05非同期アクションと派生値のパターン
- 06移行ガイドとコードテンプレート
活用シーン
- 01React アプリの状態ストア設計
- 02状態の永続化と Devtools の設定
- 03Next.js のハイドレーション不一致への対処
- 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.jsonassets/example-template.txtreferences/example-reference.mdmiddleware-guide.mdmigration-guide.mdnextjs-hydration.mdtypescript-patterns.mdrules/zustand-state-management.mdscripts/check-versions.shexample-script.shtemplates/async-actions-store.tsbasic-store.tscomputed-store.tsdevtools-store.tsnextjs-store.tspersist-store.tsslices-pattern.tstypescript-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"
出典:
- DEV Community: Next.js での persist ミドルウェア
- GitHub Discussions #2839
発生する理由: 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 型が壊れます
発生する理由:
ミドルウェアで 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.
出典:
- GitHub Discussions #2642
- Issue #2863
発生する理由: セレクター内で新しいオブジェクト参照を作成すると、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 型の推論に失敗し、複雑なミドルウェア型が壊れます
発生する理由: 複数のスライスを組み合わせる場合、ミドルウェアとの互換性を保つために、明示的な型注釈が必要です。
予防策:
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 で最新情報を確認してください。
公式ドキュメント
- Zustand:https://zustand.docs.pmnd.rs/
- GitHub:https://github.com/pmndrs/zustand
- TypeScript ガイド:https://zustand.docs.pmnd.rs/guides/typescript
- Context7 ライブラリ ID:
/pmndrs/zustand