--- name: zustand-state-management description: | Build type-safe global state in React applications with Zustand. Supports TypeScript, persist middleware, devtools, slices pattern, and Next.js SSR. Use when setting up React state, migrating from Redux/Context API, implementing localStorage persistence, or troubleshooting Next.js hydration errors, TypeScript inference issues, or infinite render loops. license: MIT --- # Zustand State Management **Status**: Production Ready ✅ **Last Updated**: 2025-10-24 **Latest Version**: zustand@5.0.8 **Dependencies**: React 18+, TypeScript 5+ --- ## Quick Start (3 Minutes) ### 1. Install Zustand ```bash npm install zustand # or pnpm add zustand # or yarn add zustand ``` **Why Zustand?** - Minimal API: Only 1 function to learn (`create`) - No boilerplate: No providers, reducers, or actions - TypeScript-first: Excellent type inference - Fast: Fine-grained subscriptions prevent unnecessary re-renders - Flexible: Middleware for persistence, devtools, and more ### 2. Create Your First Store (TypeScript) ```typescript import { create } from 'zustand' interface BearStore { bears: number increase: (by: number) => void reset: () => void } const useBearStore = create()((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), reset: () => set({ bears: 0 }), })) ``` **CRITICAL**: Notice the **double parentheses** `create()()` - this is required for TypeScript with middleware. ### 3. Use Store in Components ```tsx import { useBearStore } from './store' function BearCounter() { const bears = useBearStore((state) => state.bears) return

{bears} around here...

} function Controls() { const increase = useBearStore((state) => state.increase) return } ``` **Why this works:** - Components only re-render when their selected state changes - No Context providers needed - Selector function extracts specific state slice --- ## The 3-Pattern Setup Process ### Pattern 1: Basic Store (JavaScript) For simple use cases without TypeScript: ```javascript import { create } from 'zustand' const useStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), })) ``` **When to use:** - Prototyping - Small apps - No TypeScript in project ### Pattern 2: TypeScript Store (Recommended) For production apps with type safety: ```typescript import { create } from 'zustand' // Define store interface interface CounterStore { count: number increment: () => void decrement: () => void } // Create typed store const useCounterStore = create()((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), })) ``` **Key Points:** - Separate interface for state + actions - Use `create()()` syntax (currying for middleware) - Full IDE autocomplete and type checking ### Pattern 3: Persistent Store For state that survives page reloads: ```typescript 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()( 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 }, ), ) ``` **Why this matters:** - State automatically saved to localStorage - Restored on page reload - Works with sessionStorage too - Handles serialization automatically --- ## Critical Rules ### Always Do ✅ Use `create()()` (double parentheses) in TypeScript for middleware compatibility ✅ Define separate interfaces for state and actions ✅ Use selector functions to extract specific state slices ✅ Use `set` with updater functions for derived state: `set((state) => ({ count: state.count + 1 }))` ✅ Use unique names for persist middleware storage keys ✅ Handle Next.js hydration with `hasHydrated` flag pattern ✅ Use `shallow` for selecting multiple values ✅ Keep actions pure (no side effects except state updates) ### Never Do ❌ Use `create(...)` (single parentheses) in TypeScript - breaks middleware types ❌ Mutate state directly: `set((state) => { state.count++; return state })` - use immutable updates ❌ Create new objects in selectors: `useStore((state) => ({ a: state.a }))` - causes infinite renders ❌ Use same storage name for multiple stores - causes data collisions ❌ Access localStorage during SSR without hydration check ❌ Use Zustand for server state - use TanStack Query instead ❌ Export store instance directly - always export the hook --- ## Known Issues Prevention This skill prevents **5** documented issues: ### Issue #1: Next.js Hydration Mismatch **Error**: `"Text content does not match server-rendered HTML"` or `"Hydration failed"` **Source**: - [DEV Community: Persist middleware in Next.js](https://dev.to/abdulsamad/how-to-use-zustands-persist-middleware-in-nextjs-4lb5) - GitHub Discussions #2839 **Why It Happens**: Persist middleware reads from localStorage on client but not on server, causing state mismatch. **Prevention**: ```typescript import { create } from 'zustand' import { persist } from 'zustand/middleware' interface StoreWithHydration { count: number _hasHydrated: boolean setHasHydrated: (hydrated: boolean) => void increase: () => void } const useStore = create()( 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
Loading...
} // Now safe to render with persisted state return } ``` ### Issue #2: TypeScript Double Parentheses Missing **Error**: Type inference fails, `StateCreator` types break with middleware **Source**: [Official Zustand TypeScript Guide](https://zustand.docs.pmnd.rs/guides/typescript) **Why It Happens**: The currying syntax `create()()` is required for middleware to work with TypeScript inference. **Prevention**: ```typescript // ❌ WRONG - Single parentheses const useStore = create((set) => ({ // ... })) // ✅ CORRECT - Double parentheses const useStore = create()((set) => ({ // ... })) ``` **Rule**: Always use `create()()` in TypeScript, even without middleware (future-proof). ### Issue #3: Persist Middleware Import Error **Error**: `"Attempted import error: 'createJSONStorage' is not exported from 'zustand/middleware'"` **Source**: GitHub Discussion #2839 **Why It Happens**: Wrong import path or version mismatch between zustand and build tools. **Prevention**: ```typescript // ✅ CORRECT imports for v5 import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' // Verify versions // zustand@5.0.8 includes createJSONStorage // zustand@4.x uses different API // Check your package.json // "zustand": "^5.0.8" ``` ### Issue #4: Infinite Render Loop **Error**: Component re-renders infinitely, browser freezes **Source**: GitHub Discussions #2642 **Why It Happens**: Creating new object references in selectors causes Zustand to think state changed. **Prevention**: ```typescript 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 Pattern TypeScript Complexity **Error**: `StateCreator` types fail to infer, complex middleware types break **Source**: [Official Slices Pattern Guide](https://github.com/pmndrs/zustand/blob/main/docs/guides/slices-pattern.md) **Why It Happens**: Combining multiple slices requires explicit type annotations for middleware compatibility. **Prevention**: ```typescript 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()((...a) => ({ ...createBearSlice(...a), ...createFishSlice(...a), })) ``` --- ## Middleware Configuration ### Persist Middleware (localStorage) ```typescript import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' interface MyStore { data: string[] addItem: (item: string) => void } const useStore = create()( 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 Middleware (Redux DevTools) ```typescript import { create } from 'zustand' import { devtools } from 'zustand/middleware' interface CounterStore { count: number increment: () => void } const useStore = create()( devtools( (set) => ({ count: 0, increment: () => set( (state) => ({ count: state.count + 1 }), undefined, 'counter/increment', // Action name in DevTools ), }), { name: 'CounterStore' }, // Store name in DevTools ), ) ``` ### Combining Multiple Middlewares ```typescript import { create } from 'zustand' import { devtools, persist } from 'zustand/middleware' const useStore = create()( devtools( persist( (set) => ({ // store definition }), { name: 'my-storage' }, ), { name: 'MyStore' }, ), ) ``` **Order matters**: `devtools(persist(...))` shows persist actions in DevTools. --- ## Common Patterns ### Pattern: Computed/Derived Values ```typescript interface StoreWithComputed { items: string[] addItem: (item: string) => void // Computed in selector, not stored } const useStore = create()((set) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })), })) // Use in component function ItemCount() { const count = useStore((state) => state.items.length) return
{count} items
} ``` ### Pattern: Async Actions ```typescript interface AsyncStore { data: string | null isLoading: boolean error: string | null fetchData: () => Promise } const useAsyncStore = create()((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 }) } }, })) ``` ### Pattern: Resetting Store ```typescript interface ResettableStore { count: number name: string increment: () => void reset: () => void } const initialState = { count: 0, name: '', } const useStore = create()((set) => ({ ...initialState, increment: () => set((state) => ({ count: state.count + 1 })), reset: () => set(initialState), })) ``` ### Pattern: Selector with Params ```typescript interface TodoStore { todos: Array<{ id: string; text: string; done: boolean }> addTodo: (text: string) => void toggleTodo: (id: string) => void } const useStore = create()((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 (
toggleTodo(id)} /> {todo.text}
) } ``` --- ## Using Bundled Resources ### Templates (templates/) This skill includes 8 ready-to-use template files: - `basic-store.ts` - Minimal JavaScript store example - `typescript-store.ts` - Properly typed TypeScript store - `persist-store.ts` - localStorage persistence with migration - `slices-pattern.ts` - Modular store organization - `devtools-store.ts` - Redux DevTools integration - `nextjs-store.ts` - SSR-safe Next.js store with hydration - `computed-store.ts` - Derived state patterns - `async-actions-store.ts` - Async operations with loading states **Example Usage:** ```bash # Copy template to your project cp ~/.claude/skills/zustand-state-management/templates/typescript-store.ts src/store/ ``` **When to use each:** - Use `basic-store.ts` for quick prototypes - Use `typescript-store.ts` for most production apps - Use `persist-store.ts` when state needs to survive page reloads - Use `slices-pattern.ts` for large, complex stores (100+ lines) - Use `nextjs-store.ts` for Next.js projects with SSR ### References (references/) Deep-dive documentation for complex scenarios: - `middleware-guide.md` - Complete middleware documentation (persist, devtools, immer, custom) - `typescript-patterns.md` - Advanced TypeScript patterns and troubleshooting - `nextjs-hydration.md` - SSR, hydration, and Next.js best practices - `migration-guide.md` - Migrating from Redux, Context API, or Zustand v4 **When Claude should load these:** - Load `middleware-guide.md` when user asks about persistence, devtools, or custom middleware - Load `typescript-patterns.md` when encountering complex type inference issues - Load `nextjs-hydration.md` for Next.js-specific problems - Load `migration-guide.md` when migrating from other state management solutions ### Scripts (scripts/) - `check-versions.sh` - Verify Zustand version and compatibility **Usage:** ```bash cd your-project/ ~/.claude/skills/zustand-state-management/scripts/check-versions.sh ``` --- ## Advanced Topics ### Vanilla Store (Without React) ```typescript import { createStore } from 'zustand/vanilla' const store = createStore()((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() ``` ### Custom Middleware ```typescript import { StateCreator, StoreMutatorIdentifier } from 'zustand' type Logger = ( f: StateCreator, name?: string, ) => StateCreator const logger: Logger = (f, name) => (set, get, store) => { const loggedSet: typeof set = (...a) => { set(...(a as Parameters)) console.log(`[${name}]:`, get()) } return f(loggedSet, get, store) } // Use custom middleware const useStore = create()( logger((set) => ({ // store definition }), 'MyStore'), ) ``` ### Immer Middleware (Mutable Updates) ```typescript 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()( immer((set) => ({ todos: [], addTodo: (text) => set((state) => { // Mutate directly with Immer state.todos.push({ id: Date.now().toString(), text }) }), })), ) ``` --- ## Dependencies **Required**: - `zustand@5.0.8` - State management library - `react@18.0.0+` - React framework **Optional**: - `@types/node` - For TypeScript path resolution - `immer` - For mutable update syntax - Redux DevTools Extension - For devtools middleware --- ## Official Documentation - **Zustand**: https://zustand.docs.pmnd.rs/ - **GitHub**: https://github.com/pmndrs/zustand - **TypeScript Guide**: https://zustand.docs.pmnd.rs/guides/typescript - **Slices Pattern**: https://github.com/pmndrs/zustand/blob/main/docs/guides/slices-pattern.md - **Context7 Library ID**: `/pmndrs/zustand` --- ## Package Versions (Verified 2025-10-24) ```json { "dependencies": { "zustand": "^5.0.8", "react": "^19.0.0" }, "devDependencies": { "@types/node": "^22.0.0", "typescript": "^5.0.0" } } ``` **Compatibility**: - React 18+, React 19 ✅ - TypeScript 5+ ✅ - Next.js 14+, Next.js 15+ ✅ - Vite 5+ ✅ --- ## Troubleshooting ### Problem: Store updates don't trigger re-renders **Solution**: Ensure you're using selector functions, not destructuring: `const bears = useStore(state => state.bears)` not `const { bears } = useStore()` ### Problem: TypeScript errors with middleware **Solution**: Use double parentheses: `create()()` not `create()` ### Problem: Persist middleware causes hydration error **Solution**: Implement `_hasHydrated` flag pattern (see Issue #1) ### Problem: Actions not showing in Redux DevTools **Solution**: Pass action name as third parameter to `set`: `set(newState, undefined, 'actionName')` ### Problem: Store state resets unexpectedly **Solution**: Check if using HMR (hot module replacement) - Zustand resets on module reload in development --- ## Complete Setup Checklist Use this checklist to verify your Zustand setup: - [ ] Installed `zustand@5.0.8` or later - [ ] Created store with proper TypeScript types - [ ] Used `create()()` double parentheses syntax - [ ] Tested selector functions in components - [ ] Verified components only re-render when selected state changes - [ ] If using persist: Configured unique storage name - [ ] If using persist: Implemented hydration check for Next.js - [ ] If using devtools: Named actions for debugging - [ ] If using slices: Properly typed `StateCreator` for each slice - [ ] All actions are pure functions - [ ] No direct state mutations - [ ] Store works in production build --- **Questions? Issues?** 1. Check [references/typescript-patterns.md](references/typescript-patterns.md) for TypeScript help 2. Check [references/nextjs-hydration.md](references/nextjs-hydration.md) for Next.js issues 3. Check [references/middleware-guide.md](references/middleware-guide.md) for persist/devtools help 4. Official docs: https://zustand.docs.pmnd.rs/ 5. GitHub issues: https://github.com/pmndrs/zustand/issues