Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    ovachiever

    zustand-state-management

    ovachiever/zustand-state-management
    Coding
    19
    1 installs

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

    Build type-safe global state in React applications with Zustand...

    SKILL.md

    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

    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)

    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 }),
    }))
    

    CRITICAL: Notice the double parentheses create<T>()() - this is required for TypeScript with middleware.

    3. Use Store in Components

    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>
    }
    

    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:

    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:

    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 })),
    }))
    

    Key Points:

    • Separate interface for state + actions
    • Use create<T>()() syntax (currying for middleware)
    • Full IDE autocomplete and type checking

    Pattern 3: Persistent Store

    For state that survives page reloads:

    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
        },
      ),
    )
    

    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<T>()() (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<T>(...) (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
    • GitHub Discussions #2839

    Why It Happens: Persist middleware reads from localStorage on client but not on server, causing state mismatch.

    Prevention:

    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 />
    }
    

    Issue #2: TypeScript Double Parentheses Missing

    Error: Type inference fails, StateCreator types break with middleware

    Source: Official Zustand TypeScript Guide

    Why It Happens: The currying syntax create<T>()() is required for middleware to work with TypeScript inference.

    Prevention:

    // ❌ WRONG - Single parentheses
    const useStore = create<MyStore>((set) => ({
      // ...
    }))
    
    // ✅ CORRECT - Double parentheses
    const useStore = create<MyStore>()((set) => ({
      // ...
    }))
    

    Rule: Always use create<T>()() 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:

    // ✅ 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:

    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

    Why It Happens: Combining multiple slices requires explicit type annotations for middleware compatibility.

    Prevention:

    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),
    }))
    

    Middleware Configuration

    Persist Middleware (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 Middleware (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
      ),
    )
    

    Combining Multiple Middlewares

    import { create } from 'zustand'
    import { devtools, persist } from 'zustand/middleware'
    
    const useStore = create<MyStore>()(
      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

    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>
    }
    

    Pattern: Async Actions

    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 })
        }
      },
    }))
    

    Pattern: Resetting Store

    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),
    }))
    

    Pattern: Selector with Params

    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>
      )
    }
    

    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:

    # 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:

    cd your-project/
    ~/.claude/skills/zustand-state-management/scripts/check-versions.sh
    

    Advanced Topics

    Vanilla Store (Without 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()
    

    Custom Middleware

    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 Middleware (Mutable Updates)

    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 })
          }),
      })),
    )
    

    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)

    {
      "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<T>()() not create<T>()

    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<T>()() 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 for TypeScript help
    2. Check references/nextjs-hydration.md for Next.js issues
    3. Check 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
    Recommended Servers
    Svelte
    Svelte
    Browser tool
    Browser tool
    Vercel Grep
    Vercel Grep
    Repository
    ovachiever/droid-tings
    Files