| | |

Zustand 13 ⚛️ Cheatsheet

⏱️ Estimated read: 4 min | |

Installation

npm install zustand
# or
pnpm add zustand
# or
yarn add zustand
PackagePurpose
zustandCore store, hooks, middleware
immerRequired for immer middleware
zustand/middlewareBuilt-in middleware (persist, devtools, immer)
RequirementValue
React version18.0+
TypeScript syntaxcreate<T>()(...) (curried, v5)
ProviderNot required

Creating a Store

import { create } from 'zustand'

interface CounterState {
  count: number
  increment: () => void
}

export const useCounterStore = create<CounterState>()((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}))
RuleDetail
Curried syntaxcreate<T>()(...) — double parentheses
Module scopeStore created once, outside components
set behaviorShallow merge
get behaviorRead current state at call time
ExportThe hook, not the store object

Reading State

// Atomic selector: re-renders only when count changes
const count = useCounterStore((state) => state.count)

// Action selector: stable reference, never re-renders
const increment = useCounterStore((state) => state.increment)

// Multiple primitives: multiple hook calls
const count = useCounterStore((s) => s.count)
const name = useCounterStore((s) => s.name)

// Object selector: useShallow required in v5
import { useShallow } from 'zustand/react/shallow'
const { count, name } = useCounterStore(
  useShallow((s) => ({ count: s.count, name: s.name }))
)
PatternRe-renders When
(s) => s.countcount changes
(s) => s.actionNever
(s) => ({ a, b }) without useShallowEvery store change (v5 crash)
useShallow((s) => ({ a, b }))a or b changes
(s) => sEvery change

Updating State

// Functional form: reads current state at update time
increment: () => set((state) => ({ count: state.count + 1 }))

// Direct object: constant value
reset: () => set({ count: 0 })

// Nested object: spread every level
setName: (name) => set((state) => ({ user: { ...state.user, name } }))

// Array: new reference
addTodo: (todo) => set((state) => ({ todos: [...state.todos, todo] }))

// Array element: map + spread
toggle: (id) =>
  set((state) => ({
    todos: state.todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)),
  }))
OperationPattern
Top-level updateset({ key: value })
State-dependentset((state) => ({ ... }))
Nested object{ ...state.obj, key: value }
Array add[...state.items, newItem]
Array removestate.items.filter(...)
Array updatestate.items.map(...)
Resetset(initialState)

Middleware

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

export const useStore = create<State>()(
  devtools(                              // Outermost
    persist(                             // Middle
      immer((set) => ({                  // Innermost
        count: 0,
        increment: () => set((state) => { state.count += 1 }, false, 'counter/increment'),
      })),
      { name: 'counter-storage' }
    ),
    { name: 'CounterStore', enabled: process.env.NODE_ENV === 'development' }
  )
)
MiddlewarePurposeKey Options
persistlocalStorage/sessionStoragename, partialize, version, migrate
devtoolsRedux DevToolsname, enabled
immerMutating syntaxRequires immer package
subscribeWithSelectorSelector-based subscribe—
OrderCorrect
Outermostdevtools
Middlepersist
Innermostimmer

Persist

persist(
  (set) => ({ theme: 'light', sidebarOpen: true }),
  {
    name: 'settings-storage',
    partialize: (state) => ({ theme: state.theme }),   // only persist theme
    version: 1,
    migrate: (persisted, version) => {
      if (version === 0) return { theme: persisted.theme }
      return persisted
    },
  }
)
OptionPurpose
namelocalStorage key (required)
partializeSelect fields to persist
versionSchema version
migrateTransform old persisted state
skipHydrationDefer hydration (SSR)

Immer

immer((set) => ({
  todos: [],
  addTodo: (text) =>
    set((state) => {
      state.todos.push({ id: Date.now(), text })   // mutate draft
    }),
  toggle: (id) =>
    set((state) => {
      const todo = state.todos.find((t) => t.id === id)
      if (todo) todo.done = !todo.done
    }),
}))
RuleStatus
Mutate draft, return nothing✅ Correct
Return new value, no mutation✅ Correct
Mutate and return❌ Immer error

DevTools

increment: () =>
  set((state) => ({ count: state.count + 1 }), false, 'counter/increment')
//                                               │      │
//                                               │      └── action name
//                                               └── replace flag (false)
set ArgumentPurpose
1stPartial state or updater
2ndReplace flag (usually false)
3rdAction name for DevTools

Subscribe (Transient)

import { subscribeWithSelector } from 'zustand/middleware'

const useStore = create<State>()(
  subscribeWithSelector((set) => ({ count: 0 }))
)

// In a component
useEffect(() => {
  const unsub = useStore.subscribe(
    (state) => state.count,
    (count, prevCount) => console.log(count, prevCount)
  )
  return unsub
}, [])
MethodPurpose
store.subscribe(listener)Fires on every change
store.subscribe(selector, listener)Fires when selector result changes
store.getState()Read without subscribing
store.setState(partial)Write without a component

Testing

// Factory pattern: fresh store per test
export const createCounterStore = () =>
  create<CounterState>()((set) => ({
    count: 0,
    increment: () => set((state) => ({ count: state.count + 1 })),
  }))

// Test
const store = createCounterStore()
store.getState().increment()
expect(store.getState().count).toBe(1)
PatternPurpose
Factory functionFresh store per test
beforeEach resetsetState(initial, true)
getState()Read state after action
Extracted selectorPure function, testable in isolation

Common Pitfalls

PitfallFix
Component re-renders on every changeUse atomic selectors
v5 crash on object selectorWrap with useShallow
Nested fields disappearSpread the nested object
UI does not updateCreate new references with set
Persisted modal reopensExclude transient fields with partialize
Hydration mismatch (SSR)skipHydration: true
Actions disappear after resetDon’t use set(initialState, true) without actions
Tests share stateFactory function or beforeEach reset

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!