Claude-skills zustand-state-management
Zustand state management for React with TypeScript. Use for global state, Redux/Context API migration, localStorage persistence, slices pattern, devtools, Next.js SSR, or encountering hydration errors, TypeScript inference issues, persist middleware problems, infinite render loops.
git clone https://github.com/secondsky/claude-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/secondsky/claude-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/zustand-state-management/skills/zustand-state-management" ~/.claude/skills/secondsky-claude-skills-zustand-state-management && rm -rf "$T"
plugins/zustand-state-management/skills/zustand-state-management/SKILL.mdZustand State Management
Status: Production Ready ✅ Last Updated: 2025-11-21 Latest Version: zustand@5.0.8 Dependencies: React 18+, TypeScript 5+
Quick Start (3 Minutes)
1. Install Zustand
bun add zustand # preferred # or: npm install 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
syntax (currying for middleware)create<T>()() - 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 (5 Issues)
| Issue | Error | Quick Fix |
|---|---|---|
| #1 Hydration mismatch | "Text content does not match" | Use flag + |
| #2 TypeScript inference | Types break with middleware | Use double parentheses |
| #3 Import error | "createJSONStorage not exported" | Upgrade to zustand@5.0.8+ |
| #4 Infinite loop | Browser freezes | Use or separate selectors |
| #5 Slices types | StateCreator types fail | Explicit |
Most Critical - TypeScript double parentheses:
// ❌ WRONG: create<T>((set) => ...) // ✅ CORRECT: create<T>()((set) => ...)
See:
references/known-issues.md for complete solutions with code examples.
Middleware Configuration
import { create } from 'zustand' import { devtools, persist } from 'zustand/middleware' const useStore = create<MyStore>()( devtools( persist( (set) => ({ /* store definition */ }), { name: 'my-storage' }, ), { name: 'MyStore' }, ), )
| Middleware | Purpose | Import |
|---|---|---|
| localStorage/sessionStorage | |
| Redux DevTools integration | |
| Mutable update syntax | |
Order matters:
devtools(persist(...)) shows persist actions in DevTools.
See:
references/middleware-guide.md for complete middleware documentation.
Common Patterns
| Pattern | Use Case | Key Technique |
|---|---|---|
| Computed values | Derived data | Compute in selector: |
| Async actions | API calls | + try/catch |
| Reset store | Logout, form clear | |
| Selector with params | Dynamic access | |
| Multiple stores | Separation of concerns | Create separate calls |
See:
references/common-patterns.md for complete implementations.
Advanced Topics
| Topic | Use Case | Key API |
|---|---|---|
| Vanilla store | Non-React, testing | from |
| Custom middleware | Logging, timestamps | Wrap |
| Immer | Mutable update syntax | middleware |
| Subscriptions | Side effects | |
See:
references/advanced-topics.md for complete implementations.
Bundled Resources
| Type | Files |
|---|---|
| Templates | , , , , , , , |
| References | , , , , , , |
When to Load References
| Reference | Load When... |
|---|---|
| Debugging hydration, TypeScript, infinite loop, or slices errors |
| Implementing computed values, async actions, reset patterns |
| Vanilla stores, custom middleware, Immer, subscriptions |
| Configuring persist, devtools, or combining middlewares |
| Complex type inference issues, StateCreator problems |
| Next.js SSR/hydration problems |
| Migrating from Redux, Context API, or Zustand v4 |
Quick Troubleshooting
| Problem | Solution |
|---|---|
| Store updates don't trigger re-renders | Use selector: not destructuring |
| TypeScript errors with middleware | Use double parentheses |
| Hydration error with persist | Implement flag pattern |
| Actions not showing in DevTools | Pass action name: |
| Store resets unexpectedly | HMR causes reset in development |
Dependencies
{ "dependencies": { "zustand": "^5.0.8", "react": "^18.0.0+" } }
Compatibility: React 18+, React 19, TypeScript 5+, Next.js 14+, Vite 5+
Official Docs: https://zustand.docs.pmnd.rs/ | GitHub: https://github.com/pmndrs/zustand