zustand-state-management
Install this skill
npx skills add jezweb/claude-skillsWorks across Claude Code, Cursor, Codex, Copilot & Antigravity
Zustand is a minimalist state management library for React and React Native. It operates on a subscription model, allowing components to selectively re-render only when the specific slice of state they consume changes. Unlike Context API, it does not require provider wrapping. Its API centers around a hook-based store that remains accessible anywhere in the component tree. Zustand emphasizes immutable updates and strictly separated actions, ensuring predictable data flow. The library supports middleware for persistence and devtools integration while maintaining a tiny footprint. By leveraging the curried create function, it provides strong TypeScript inference, preventing common type-related bugs. This library is effective for managing everything from simple UI toggles to complex global state without the boilerplate often associated with Redux.
When to Use This Skill
- β’Maintaining user authentication tokens across a SPA
- β’Synchronizing UI theme preferences across sessions
- β’Handling complex form state that persists during navigation
- β’Managing real-time dashboard data updates
- β’Sharing state between deeply nested components without prop drilling
How to Invoke This Skill
Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:
- βHow to manage global state in React without Context
- βSetup Zustand with TypeScript correctly
- βPersist Zustand store to localStorage in Next.js
- βPrevent unnecessary re-renders in Zustand
- βZustand store vs TanStack Query for data fetching
Pro Tips
- π‘Always use the `create<T>()()` double parentheses syntax for TypeScript stores to ensure robust type inference and safety.
- π‘Leverage selector functions (`useStore(state => state.value)`) to prevent unnecessary component re-renders, ensuring components only update when their specific slice of state changes.
- π‘For complex asynchronous actions or side effects, consider integrating middleware or custom actions directly within your store to keep logic centralized and testable.
What this skill does
- β’Atomic state selection to prevent unnecessary component re-renders
- β’Persist middleware for automatic synchronization with browser storage
- β’Middleware support for logging, devtools, and hydration tracking
- β’Curried TypeScript store creation for accurate type inference
- β’Decoupled store architecture allowing non-hook state access
When not to use it
- βManaging server-side cache and remote data fetching
- βStoring massive datasets that require frequent high-frequency updates
- βStandard local component state that doesn't need to be shared
Example workflow
- Define a TypeScript interface representing the store state and actions
- Create the store using the curried create function to ensure type safety
- Implement store actions as pure functions that return updated state
- Use selector functions within components to target specific state slices
- Apply the persist middleware to automatically save state to browser storage
- Handle hydration checks in Next.js to prevent mismatch errors during SSR
Prerequisites
- βReact 18 or 19
- βTypeScript 5.0+
Pitfalls & limitations
- !Selecting entire objects instead of specific properties causes performance issues
- !Accessing localStorage in server-rendered environments leads to hydration errors
- !Direct state mutation bypassing the set function creates silent bugs
- !Using single parentheses in TypeScript breaks middleware type inference
FAQ
How it compares
Unlike manual Context API implementations, Zustand minimizes component re-renders through targeted subscriptions and avoids the deep tree nesting required by Providers.
π Full skill instructions β original source: jezweb/claude-skills
**Last Updated**: 2026-01-21
**Latest Version**: [email protected] (released 2026-01-12)
**Dependencies**: React 18-19, TypeScript 5+
---
## Quick Start
npm install zustand**TypeScript Store** (CRITICAL: use
create<T>()() double parentheses):import { create } from 'zustand'
interface BearStore {
bears: number
increase: (by: number) => void
}
const useBearStore = create<BearStore>()((set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
}))**Use in Components**:
const bears = useBearStore((state) => state.bears) // Only re-renders when bears changes
const increase = useBearStore((state) => state.increase)---
## Core Patterns
**Basic Store** (JavaScript):
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))**TypeScript Store** (Recommended):
interface CounterStore { count: number; increment: () => void }
const useStore = create<CounterStore>()((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))**Persistent Store** (survives page reloads):
import { persist, createJSONStorage } from 'zustand/middleware'
const useStore = create<UserPreferences>()(
persist(
(set) => ({ theme: 'system', setTheme: (theme) => set({ theme }) }),
{ name: 'user-preferences', storage: createJSONStorage(() => localStorage) },
),
)---
## 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
useShallow hook 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 **6** 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**:
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](https://zustand.docs.pmnd.rs/guides/typescript)
**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
// [email protected] includes createJSONStorage
// [email protected] uses different API
// Check your package.json
// "zustand": "^5.0.9"### Issue #4: Infinite Render Loop
**Error**: Component re-renders infinitely, browser freezes
Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate.**Source**:
- GitHub Discussions #2642
- [Issue #2863](https://github.com/pmndrs/zustand/issues/2863)
**Why It Happens**:
Creating new object references in selectors causes Zustand to think state changed.
**v5 Breaking Change**: Zustand v5 made this error MORE explicit compared to v4. In v4, this behavior was "non-ideal" but could go unnoticed. In v5, you'll immediately see the "Maximum update depth exceeded" error.
**Prevention**:
import { useShallow } 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 useShallow hook for multiple values
const { bears, fishes } = useStore(
useShallow((state) => ({ bears: state.bears, fishes: state.fishes }))
)### 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**:
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),
}))### Issue #6: Persist Middleware Race Condition (Fixed v5.0.10+)
**Error**: Inconsistent state during concurrent rehydration attempts
**Source**:
- [GitHub PR #3336](https://github.com/pmndrs/zustand/pull/3336)
- [Release v5.0.10](https://github.com/pmndrs/zustand/releases/tag/v5.0.10)
**Why It Happens**:
In Zustand v5.0.9 and earlier, concurrent calls to rehydrate during persist middleware initialization could cause a race condition where multiple hydration attempts would interfere with each other, leading to inconsistent state.
**Prevention**:
Upgrade to Zustand v5.0.10 or later. No code changes needed - the fix is internal to the persist middleware.
npm install zustand@latest # Ensure v5.0.10+**Note**: This was fixed in v5.0.10 (January 2026). If you're using v5.0.9 or earlier and experiencing state inconsistencies with persist middleware, upgrade immediately.
---
## Middleware
**Persist** (localStorage):
import { persist, createJSONStorage } from 'zustand/middleware'
const useStore = create<MyStore>()(
persist(
(set) => ({ data: [], addItem: (item) => set((state) => ({ data: [...state.data, item] })) }),
{
name: 'my-storage',
partialize: (state) => ({ data: state.data }), // Only persist 'data'
},
),
)**Devtools** (Redux DevTools):
import { devtools } from 'zustand/middleware'
const useStore = create<CounterStore>()(
devtools(
(set) => ({ count: 0, increment: () => set((s) => ({ count: s.count + 1 }), undefined, 'increment') }),
{ name: 'CounterStore' },
),
)**v4βv5 Migration Note**: In Zustand v4, devtools was imported from
'zustand/middleware/devtools'. In v5, use 'zustand/middleware' (as shown above). If you see "Module not found: Can't resolve 'zustand/middleware/devtools'", update your import path.**Combining Middlewares** (order matters):
const useStore = create<MyStore>()(devtools(persist((set) => ({ /* ... */ }), { name: 'storage' }), { name: 'MyStore' }))---
## Common Patterns
**Computed/Derived Values** (in selector, not stored):
const count = useStore((state) => state.items.length) // Computed on read**Async Actions**:
const useAsyncStore = create<AsyncStore>()((set) => ({
data: null,
isLoading: false,
fetchData: async () => {
set({ isLoading: true })
const response = await fetch('/api/data')
set({ data: await response.text(), isLoading: false })
},
}))**Resetting Store**:
const initialState = { count: 0, name: '' }
const useStore = create<ResettableStore>()((set) => ({
...initialState,
reset: () => set(initialState),
}))**Selector with Params**:
const todo = useStore((state) => state.todos.find((t) => t.id === id))---
## Bundled Resources
**Templates**:
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**References**:
middleware-guide.md (persist/devtools/immer/custom), typescript-patterns.md (type inference issues), nextjs-hydration.md (SSR/hydration), migration-guide.md (from Redux/Context/v4)**Scripts**:
check-versions.sh (version compatibility)---
## Advanced Topics
**Vanilla Store** (Without React):
import { createStore } from 'zustand/vanilla'
const store = createStore<CounterStore>()((set) => ({ count: 0, increment: () => set((s) => ({ count: s.count + 1 })) }))
const unsubscribe = store.subscribe((state) => console.log(state.count))
store.getState().increment()**Custom Middleware**:
const logger: Logger = (f, name) => (set, get, store) => {
const loggedSet: typeof set = (...a) => { set(...a); console.log([${name}]:, get()) }
return f(loggedSet, get, store)
}**Immer Middleware** (Mutable Updates):
import { immer } from 'zustand/middleware/immer'
const useStore = create<TodoStore>()(immer((set) => ({
todos: [],
addTodo: (text) => set((state) => { state.todos.push({ id: Date.now().toString(), text }) }),
})))**v5.0.3βv5.0.4 Migration Note**: If upgrading from v5.0.3 to v5.0.4+ and immer middleware stops working, verify you're using the import path shown above (
zustand/middleware/immer). Some users reported issues after the v5.0.4 update that were resolved by confirming the correct import.**Experimental SSR Safe Middleware** (v5.0.9+):
**Status**: Experimental (API may change)
Zustand v5.0.9 introduced experimental
unstable_ssrSafe middleware for Next.js usage. This provides an alternative approach to the _hasHydrated pattern (see Issue #1).import { unstable_ssrSafe } from 'zustand/middleware'
const useStore = create<Store>()(
unstable_ssrSafe(
persist(
(set) => ({ /* state */ }),
{ name: 'my-store' }
)
)
)**Recommendation**: Continue using the
_hasHydrated pattern documented in Issue #1 until this API stabilizes. Monitor [Discussion #2740](https://github.com/pmndrs/zustand/discussions/2740) for updates on when this becomes stable.---
## Official Documentation
- **Zustand**: https://zustand.docs.pmnd.rs/
- **GitHub**: https://github.com/pmndrs/zustand
- **TypeScript Guide**: https://zustand.docs.pmnd.rs/guides/typescript
- **Context7 Library ID**:
/pmndrs/zustand---
---
paths: "**/*.ts", "**/*.tsx", "**/*store*.ts", "**/*state*.ts"
---
# Zustand v5 Corrections
Claude's training may reference v4 patterns. This project uses **Zustand v5**.
## TypeScript: Double Parentheses Required
/* β v4 syntax (breaks middleware types) */
const useStore = create<MyState>((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
}))
/* β
v5 syntax: create<T>()(...) */
const useStore = create<MyState>()((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
}))## Persist: Import from Middleware
/* β Wrong import */
import { persist, createJSONStorage } from 'zustand'
/* β
Import from middleware */
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
const useStore = create<MyState>()(
persist(
(set) => ({ /* ... */ }),
{
name: 'my-store',
storage: createJSONStorage(() => localStorage),
}
)
)## Selectors: Don't Create Objects
/* β Creates new object every render (infinite re-renders) */
const { count, increment } = useStore((s) => ({
count: s.count,
increment: s.increment,
}))
/* β
Option 1: Select separately */
const count = useStore((s) => s.count)
const increment = useStore((s) => s.increment)
/* β
Option 2: Use shallow comparator */
import { useShallow } from 'zustand/shallow'
const { count, increment } = useStore(
useShallow((s) => ({ count: s.count, increment: s.increment }))
)## Next.js Hydration Mismatch
/* β "Text content does not match" error */
// Persist reads localStorage on client, not server
/* β
Add hydration check */
interface MyState {
count: number
_hasHydrated: boolean
setHasHydrated: (state: boolean) => void
}
const useStore = create<MyState>()(
persist(
(set) => ({
count: 0,
_hasHydrated: false,
setHasHydrated: (state) => set({ _hasHydrated: state }),
}),
{
name: 'my-store',
onRehydrateStorage: () => (state) => {
state?.setHasHydrated(true)
},
}
)
)
// In component:
const hasHydrated = useStore((s) => s._hasHydrated)
if (!hasHydrated) return <Loading />## Slices Pattern: Explicit Types
/* β
Complex but necessary for middleware */
import { StateCreator } from 'zustand'
interface BearSlice {
bears: number
addBear: () => void
}
const createBearSlice: StateCreator<
BearSlice & FishSlice, // Combined state
[],
[],
BearSlice // This slice
> = (set) => ({
bears: 0,
addBear: () => set((s) => ({ bears: s.bears + 1 })),
})## Quick Fixes
| If Claude suggests... | Use instead... |
|----------------------|----------------|
|
create<T>((set) => ...) | create<T>()((set) => ...) || Import persist from 'zustand' | Import from 'zustand/middleware' |
| Object in selector | Select separately or use
useShallow || Next.js hydration error | Add
_hasHydrated pattern || Simple slices types | Use explicit
StateCreator types |How to Use This Skill Unit
Option A: Project-Specific (Recommended)
- Click "Download" above
- In your project, create the directory:
.agent/skills/zustand-state-management/ - Save the file as
SKILL.md - The agent will automatically discover the skill based on its description.
Option B: Global Installation (All Agents)
Save the file to these locations to make it available across all projects:
- Claude Code:
~/.claude/skills/jezweb/claude-skills/zustand-state-management/SKILL.md - Cursor:
~/.cursor/skills/jezweb/claude-skills/zustand-state-management/SKILL.md - Antigravity:
~/.gemini/antigravity/skills/jezweb/claude-skills/zustand-state-management/SKILL.md
π Install with CLI:npx skills add jezweb/claude-skills

