zustand-v5

star 6

Zustand v5 state management reference. Auto-loads when working with zustand stores, create, createStore, useShallow, persist, devtools, immer middleware.

nklisch By nklisch schedule Updated 6/15/2026

name: zustand-v5 description: > Zustand v5 state management reference. Auto-loads when working with zustand stores, create, createStore, useShallow, persist, devtools, immer middleware.

Zustand Reference

Version: 5.x Docs: https://zustand.docs.pmnd.rs/

Imports

// Core store creation
import { create } from 'zustand'

// Vanilla store (non-React)
import { createStore } from 'zustand/vanilla'
import { useStore } from 'zustand'

// Middleware
import { devtools, persist, createJSONStorage } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'

// Shallow equality for multiple selectors
import { useShallow } from 'zustand/react/shallow'

// Types
import type { StoreApi, UseBoundStore, StateCreator } from 'zustand'

API Quick Reference

Basic Store

interface State {
  value: number
  increment: () => void
  decrement: () => void
}

const useStore = create<State>((set) => ({
  value: 0,
  increment: () => set((state) => ({ value: state.value + 1 })),
  decrement: () => set({ value: 0 })
}))

Store with Middleware (double parentheses required)

const useStore = create<State>()(
  devtools(
    persist(
      (set) => ({ /* state */ }),
      { name: 'storage-key' }
    ),
    { name: 'DevTools Name' }
  )
)

Selectors

// Select single value
const value = useStore((state) => state.value)

// Select multiple with shallow equality
const { value, increment } = useStore(
  useShallow((state) => ({ value: state.value, increment: state.increment }))
)

// Entire store
const state = useStore()

set() Function

set((state) => ({ count: state.count + 1 }))  // Updater function
set({ count: 0 })                              // Direct merge
set({ count: 0 }, true)                        // Replace (don't merge)

Vanilla Store

const store = createStore<State>((set) => ({ /* state */ }))

store.getState()
store.setState({ value: 5 })
store.subscribe((state) => console.log(state))

// Use in React
function Component() {
  const value = useStore(store, (state) => state.value)
  return <div>{value}</div>
}

Persist Middleware

persist(
  (set) => ({ /* state */ }),
  {
    name: 'storage-key',
    storage: createJSONStorage(() => localStorage),
    partialize: (state) => ({ field: state.field }),
    merge: (persisted, current) => ({ ...current, ...persisted }),
    onRehydrateStorage: (state) => (state, error) => {
      if (error) console.error(error)
    }
  }
)

DevTools Middleware

devtools(
  (set) => ({ /* state */ }),
  { name: 'Store Name' }
)

Immer Middleware

immer((set) => ({
  user: { name: 'John' },
  updateName: (name: string) => set((state) => {
    state.user.name = name  // Mutate directly
  })
}))

Gotchas & Version Caveats

Double parentheses with middlewarecreate<State>()() when using middleware, single create<State>() without.

useShallow for multiple selections — Without it, selecting multiple values causes re-renders on any state change. v5 changed default selector behavior to referential equality.

Don't nest immer inside devtools — Use devtools(immer(...)), not immer(devtools(...)). Immer must be innermost.

Middleware order matters — Recommended: devtools(persist(immer(...))). Outer middleware wraps inner.

set() merges by default — Use set(state, true) to replace instead of merge.

Stores must be defined outside components — Defining stores inside components causes re-creation on every render.

Selectors must be stable — Returning new object/array references on every call causes infinite loops. Use useShallow for derived objects.

Actions are stable — Functions in the store don't change reference, so selecting them doesn't cause re-renders.

persist hydration is async — State may not be loaded immediately. Use onRehydrateStorage to detect completion or track hasHydrated flag.

partialize doesn't affect initial state — Non-persisted fields still need defaults in the store creator.

merge() receives partial persisted state — Always provide fallbacks for missing fields.

Common Patterns

Persist with Hydration Tracking

interface State {
  hasHydrated: boolean
  setHasHydrated: (hydrated: boolean) => void
  // ... other state
}

create<State>()(
  persist(
    (set) => ({
      hasHydrated: false,
      setHasHydrated: (hydrated) => set({ hasHydrated: hydrated }),
      // ... other state
    }),
    {
      name: 'my-storage-key',
      partialize: (state) => ({ /* only persisted fields */ }),
      onRehydrateStorage: () => (state) => {
        state?.setHasHydrated(true)
      }
    }
  )
)

Safe Merge with Validation

persist(
  (set) => ({ /* state */ }),
  {
    merge: (persistedState, currentState) => {
      const persisted = persistedState as Partial<State> | undefined
      return {
        ...currentState,
        value: clampValue(persisted?.value ?? DEFAULT_VALUE)
      }
    }
  }
)

Selector Hooks

// Export convenience selectors
export const useStatus = (topic: string) =>
  useStore((state) => state.getStatus(topic))

export const useItems = () =>
  useStore((state) => state.items)

Anti-Patterns

Store inside component

// Bad - recreates store on every render
function Component() {
  const useStore = create<State>((set) => ({ /* state */ }))
}

// Good - define at module level
const useStore = create<State>((set) => ({ /* state */ }))
function Component() {
  const value = useStore((state) => state.value)
}

Selecting multiple without useShallow

// Bad - re-renders on any state change
const { value, count } = useStore((state) => ({
  value: state.value,
  count: state.count
}))

// Good - only re-renders when value or count change
const { value, count } = useStore(
  useShallow((state) => ({ value: state.value, count: state.count }))
)

Returning new references

// Bad - creates new array every time
const items = useStore((state) => state.items.filter(x => x.active))

// Good - compute in component body
function Component() {
  const items = useStore((state) => state.items)
  const activeItems = items.filter(x => x.active)
}

Storing derived state

// Bad - redundant state
const useStore = create((set) => ({
  firstName: 'John',
  lastName: 'Doe',
  fullName: 'John Doe'
}))

// Good - compute in selector
const useStore = create((set) => ({
  firstName: 'John',
  lastName: 'Doe'
}))
const fullName = useStore((state) => `${state.firstName} ${state.lastName}`)

Wrong middleware order

// Bad - immer outside devtools breaks debugging
immer(devtools((set) => ({ /* state */ })))

// Good - immer innermost
devtools(persist(immer((set) => ({ /* state */ }))))

Mutating without immer

// Bad - direct mutation without immer
set((state) => {
  state.user.name = name
  return state
})

// Good - with immer
immer((set) => ({
  updateName: (name) => set((state) => {
    state.user.name = name
  })
}))

// Also good - without immer, return new object
set((state) => ({
  user: { ...state.user, name }
}))
Install via CLI
npx skills add https://github.com/nklisch/skills --skill zustand-v5
Repository Details
star Stars 6
call_split Forks 4
navigation Branch main
article Path SKILL.md
More from Creator