zustand-state-management について
React プロジェクトで Zustand を使うための Claude スキルです。TypeScript、永続化、ミドルウェア、Next.js のハイドレーションを解説し、テンプレートとトラブルシューティングも含みます。
主な機能
- 01型付き・基本的なストアパターン
- 02セレクターと不変更新
- 03永続化とDevToolsミドルウェア
- 04Next.jsのハイドレーションガイド
- 05テンプレートとトラブルシューティング
活用シーン
- 01ReactアプリでZustandストアを作成
- 02再読み込み後も設定を保持
- 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"
出典:
- 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.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 の型推論に失敗し、複雑なミドルウェアの型が機能しません
発生する理由: 複数のスライスを組み合わせるには、ミドルウェアとの互換性を保つために明示的な型注釈が必要です。
防止方法:
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 })
}),
})),
)
依存関係
必須:
[email protected]- 状態管理ライブラリ[email protected]+- React フレームワーク
任意:
@types/node- TypeScript のパス解決用immer- 可変更新構文用- Redux DevTools Extension - devtools ミドルウェア用
公式ドキュメント
- Zustand:https://zustand.docs.pmnd.rs/
- GitHub:https://github.com/pmndrs/zustand
- TypeScript ガイド:https://zustand.docs.pmnd.rs/guides/typescript
- Slices パターン:https://github.com/pmndrs/zustand/blob/main/docs/guides/slices-pattern.md
- Context7 ライブラリ ID:
/pmndrs/zustand
パッケージのバージョン(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に適切な型を指定した - すべてのアクションが純粋関数である
- 状態を直接変更していない
- ストアが本番ビルドで動作する
質問や問題がありますか?
- TypeScript に関するヘルプは references/typescript-patterns.md を確認してください
- Next.js に関する問題は references/nextjs-hydration.md を確認してください
- persist/devtools に関するヘルプは references/middleware-guide.md を確認してください
- 公式ドキュメント:https://zustand.docs.pmnd.rs/
- GitHub の問題:https://github.com/pmndrs/zustand/issues