---
name: zustand-state-manager
description: "Zustand stores — slices, persistence, devtools, immer middleware, selectors that don't re-render. Production Zustand patterns: store composition with slices, persist + storage adapters, immer for nested mutations, atomic selectors (shallow + custom), zustand-x for derived state, and the SSR-safe hydration pattern for Next.js."
license: MIT
---

# Zustand State Manager

A focused skill for building production Zustand stores the way the library is actually meant to be used: composed slices, middleware stacks, and selectors that don't trigger needless re-renders. It's aimed at React and Next.js developers who have outgrown a single flat `create()` store and want patterns that scale without a Redux-sized rewrite.

## When to use it
- Your store has grown past one file and you want to split it into independent slices that still share one root state.
- Components re-render on every store change and you need fine-grained, atomic subscriptions.
- You need state to survive reloads via `localStorage`, `sessionStorage`, AsyncStorage, or IndexedDB.
- You're on the Next.js App Router and hit hydration mismatches or "store shared between requests" bugs.
- You want time-travel debugging through Redux DevTools, or ergonomic nested updates without spread soup.

## What it does
The skill applies the canonical middleware-composed API: `create()(devtools(persist(immer(...))))`. It generates **slice creators** typed as `StateCreator<RootState, [], [], Slice>` and merges them in one root store. For reads it favors atomic selectors — `useStore(s => s.count)` — plus `useShallow` for object/array selections and `createWithEqualityFn` when you need a custom comparator. Persistence is wired through `persist` with `createJSONStorage`, `partialize` to whitelist keys, `version` + `migrate` for schema changes, and `onRehydrateStorage` callbacks. `immer` middleware enables draft-style mutations inside `set`. For Next.js it uses the per-request store factory pattern with a React context provider so server and client stay isolated.

## Usage

```ts
import { create } from 'zustand'
import { devtools, persist, createJSONStorage } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'
import { useShallow } from 'zustand/react/shallow'

const useStore = create<CartState>()(
  devtools(
    persist(
      immer((set) => ({
        items: [],
        add: (p) => set((s) => { s.items.push(p) }),  // immer draft
      })),
      { name: 'cart', storage: createJSONStorage(() => localStorage),
        partialize: (s) => ({ items: s.items }) }
    )
  )
)

// only re-renders when ids change
const ids = useStore(useShallow((s) => s.items.map((i) => i.id)))
```

## Tips & gotchas
- Always call `create<T>()(...)` with the empty parens — that curried form is required for TypeScript to infer middleware types.
- `useShallow` only does a one-level shallow compare; for deeper structures reach for `createWithEqualityFn` with a custom `equalityFn`.
- Selecting a freshly built object/array (e.g. `s => ({a, b})`) without `useShallow` causes infinite re-renders — the reference changes every call.
- Bump `persist`'s `version` whenever the persisted shape changes and supply `migrate`, or rehydration silently fails.

## FAQ
**Do I need Redux Toolkit instead?** No. Zustand covers slices, devtools, and persistence without actions/reducers boilerplate or a Provider (except the SSR pattern).

**How do I read state outside React?** Use `useStore.getState()` / `useStore.setState()` / `useStore.subscribe()` — the vanilla store works anywhere.

**Why hydration errors in Next.js?** A module-level store leaks across requests. Use a per-request factory + context provider so each render gets a fresh store.
