zustand-state-management

Type-safe global state management for React with persist, devtools, and Next.js SSR support. Supports TypeScript with double-parentheses syntax ( create<T>()() ), persist middleware for localStorage, and Redux DevTools integration Prevents 6 documented errors including hydration mismatches, infinite render loops, and persist race conditions (fixed in v5.0.10+) Includes slices pattern for modular stores, vanilla store creation without React, and immer middleware for mutable-style updates Handles Next.js SSR with _hasHydrated flag pattern and experimental unstable_ssrSafe middleware to avoid client-server state mismatches

INSTALLATION
npx skills add https://github.com/jezweb/claude-skills --skill zustand-state-management
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

Zustand State Management

Last Updated: 2026-01-21

Latest Version: zustand@5.0.10 (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()((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

// zustand@5.0.9 includes createJSONStorage

// zustand@4.x 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 &#x26; 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 &#x26; FishSlice,

[],

[],

FishSlice

= (set) => ({

fishes: 0,

addFish: () => set((state) => ({ fishes: state.fishes + 1 })),

})

// Combine slices

const useStore = create<BearSlice &#x26; 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' }

)

)

)

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card