zustand-state-management について
React と TypeScript アプリで Zustand の状態ストアを構築するための実用スキルです。型付きストアや永続化、セレクター、ミドルウェア、一般的なパターンとトラブルシューティングを扱います。
主な機能
- 01型付きストアの設定
- 02セレクターによる状態アクセス
- 03localStorage または sessionStorage での永続化
- 04devtools などのミドルウェア
- 05パターンとトラブルシューティングの資料
活用シーン
- 01React アプリへのクライアント状態の追加
- 02再読み込み後もユーザー設定を保持
- 03TypeScript のミドルウェアやハイドレーションの問題調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ secondsky/ claude-skills --skill zustand-state-managementインストール後、Claude Code で/zustand-state-managementを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
assets/example-template.txtreferences/advanced-topics.mdcommon-patterns.mdexample-reference.mdknown-issues.mdmiddleware-guide.mdmigration-guide.mdnextjs-hydration.mdtypescript-patterns.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-08-03 最新バージョン:[email protected] 依存関係:React 18+、TypeScript 5+
クイックスタート(3 分)
1. Zustand をインストールする
bun add zustand # preferred
# or: npm install 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>(...)(括弧が 1 組)を使わないでください。ミドルウェアの型が壊れます
❌ 状態を直接変更しないでください:set((state) => { state.count++; return state })。不変な更新を使います
❌ セレクター内で新しいオブジェクトを作成しないでください:useStore((state) => ({ a: state.a }))。無限に再レンダリングされます
❌ 複数のストアで同じストレージ名を使わないでください。データが衝突します
❌ ハイドレーションの確認なしに SSR 中に localStorage にアクセスしないでください
❌ サーバー状態に Zustand を使わないでください。代わりに TanStack Query を使います
❌ ストアのインスタンスを直接エクスポートしないでください。必ずフックをエクスポートします
既知の問題の予防(5 件)
| 問題 | エラー | 簡単な解決方法 |
|---|---|---|
| #1 ハイドレーションの不一致 | "Text content does not match" | _hasHydrated フラグと onRehydrateStorage を使います |
| #2 TypeScript の型推論 | ミドルウェア使用時に型が壊れる | create<T>()() の二重の括弧を使います |
| #3 インポートエラー | "createJSONStorage not exported" | [email protected]+ にアップグレードします |
| #4 無限ループ | ブラウザーがフリーズする | shallow または個別のセレクターを使います |
| #5 スライスの型 | StateCreator の型が機能しない | StateCreator<Combined, [], [], Slice> を明示的に指定します |
最も重要な点 - TypeScript では二重の括弧を使います:
// ❌ WRONG: create<T>((set) => ...)
// ✅ CORRECT: create<T>()((set) => ...)
参照:コード例を含む解決方法の全容は references/known-issues.md をご覧ください。
ミドルウェアの設定
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
const useStore = create<MyStore>()(
devtools(
persist(
(set) => ({ /* store definition */ }),
{ name: 'my-storage' },
),
{ name: 'MyStore' },
),
)
| ミドルウェア | 目的 | インポート |
|---|---|---|
persist | localStorage/sessionStorage | zustand/middleware |
devtools | Redux DevTools との統合 | zustand/middleware |
immer | ミュータブルな更新構文 | zustand/middleware/immer |
順序が重要です:devtools(persist(...)) とすると、persist のアクションが DevTools に表示されます。
参照:ミドルウェアの詳細については references/middleware-guide.md をご覧ください。
よく使われるパターン
| パターン | 用途 | 主なテクニック |
|---|---|---|
| 計算値 | 派生データ | セレクター内で計算:state.items.length |
| 非同期アクション | API 呼び出し | set({ isLoading: true }) + try/catch |
| ストアのリセット | ログアウト、フォームのクリア | set(initialState) |
| パラメーター付きセレクター | 動的なアクセス | state.todos.find(t => t.id === id) |
| 複数のストア | 関心事の分離 | 別々の create() 呼び出しを作成 |
参照先:完全な実装については references/common-patterns.md をご覧ください。
高度なトピック
| トピック | 用途 | 主な API |
|---|---|---|
| バニラストア | React を使わない環境、テスト | zustand/vanilla の createStore() |
| カスタムミドルウェア | ログ記録、タイムスタンプ | StateCreator をラップ |
| Immer | 可変形式の更新構文 | immer() ミドルウェア |
| サブスクリプション | 副作用 | store.subscribe() |
参照先:完全な実装については references/advanced-topics.md をご覧ください。
同梱リソース
| 種類 | ファイル |
|---|---|
| テンプレート | 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、typescript-patterns.md、nextjs-hydration.md、migration-guide.md、known-issues.md、common-patterns.md、advanced-topics.md |
リファレンスを読み込むタイミング
| リファレンス | 読み込むタイミング |
|---|---|
known-issues.md | ハイドレーション、TypeScript、無限ループ、またはスライスに関するエラーをデバッグするとき |
common-patterns.md | 計算値、非同期アクション、リセットパターンを実装するとき |
advanced-topics.md | バニラストア、カスタムミドルウェア、Immer、サブスクリプションを扱うとき |
middleware-guide.md | persist、devtools の設定や、ミドルウェアの組み合わせを行うとき |
typescript-patterns.md | 複雑な型推論の問題や、StateCreator に関する問題があるとき |
nextjs-hydration.md | Next.js の SSR/ハイドレーションに関する問題があるとき |
migration-guide.md | Redux、Context API、または Zustand v4 から移行するとき |
トラブルシューティングの早見表
| 問題 | 解決策 |
|---|---|
| ストアを更新しても再レンダリングが発生しない | 分割代入ではなく、セレクターを使用します:useStore(state => state.value) |
| ミドルウェアで TypeScript エラーが発生する | create<T>()() の二重括弧を使用します |
| persist でハイドレーションエラーが発生する | _hasHydrated フラグパターンを実装します |
| DevTools にアクションが表示されない | アクション名を渡します:set(newState, undefined, 'actionName') |
| ストアが予期せずリセットされる | 開発中は HMR によってリセットが発生します |
依存関係
{ "dependencies": { "zustand": "^5.0.14", "react": "^18.0.0+" } }
互換性:React 18 以降、React 19、TypeScript 5 以降、Next.js 14 以降、Vite 5 以降
公式ドキュメント:https://zustand.docs.pmnd.rs/ | GitHub:https://github.com/pmndrs/zustand