# Zustand User Guide

Zustand is a small, fast, and unopinionated state-management library for React
and vanilla JavaScript. The name is German for "state". A single `create` call
returns a hook that doubles as the store API — components subscribe to slices
they care about, and only those components re-render when the slice changes.

This guide teaches you how to install Zustand, build a store, select state,
write actions (synchronous and asynchronous), compose stores from slices, apply
each shipped middleware, and use the library safely in TypeScript. Architecture,
internals, and SSR mechanics live in the Admin Guide.

## 1. Quick Start

Zustand replaces Redux/MobX/Context boilerplate with a single hook. There is
no provider, no reducer pattern (unless you want one), and no derived state
machinery. State lives in a closure; subscribers are a `Set` of listeners;
React reads via `useSyncExternalStore`.

**Key features at a glance:**

- One function — `create` — produces both the hook and the imperative `StoreApi`.
- Selector-based subscriptions: components only re-render when the selected
  slice changes (`Object.is` comparison by default).
- Works with React, React Native, and any non-React environment via
  `zustand/vanilla`.
- Six middleware shipped in the box: `persist`, `devtools`, `immer`, `redux`,
  `subscribeWithSelector`, `combine`, plus the experimental
  `unstable_ssrSafe`.
- TypeScript-first: middleware types compose through a `StoreMutators`
  registry so `setState` overloads stay accurate after stacking middleware.
- Zero dependencies for core; `use-sync-external-store` is a peer dependency.
- Tree-shakable ESM and CJS builds.

**Prerequisites**: Node.js >= 12.20.0, React >= 18 (peer), and a TypeScript
version >= 4.5 if you use TypeScript.

**Install** (pick your package manager):

```bash
npm install zustand
```

**Minimal store** — counter in 8 lines:

```tsx
import { create } from 'zustand'

type CounterState = {
  count: number
  inc: () => void
}

const useStore = create<CounterState>((set) => ({
  count: 0,
  inc: () => set((state) => ({ count: state.count + 1 })),
}))
```

**Use it in a component:**

```tsx
function Counter() {
  const count = useStore((s) => s.count)
  const inc = useStore((s) => s.inc)
  return <button onClick={inc}>{count}</button>
}
```

Expected behavior on click: the displayed number increments by 1 and only the
`Counter` component re-renders, not its parent.

> [!TIP]
> You can read the entire state at once by calling the hook with no arguments:
> `const { count, inc } = useStore()`. It works, but every state change will
> re-render that component. Prefer selectors for performance.

**Try next:** read the Selectors section, then jump to Middleware to add
persistence or Redux DevTools.

## 2. Installation

Zustand ships as a single npm package with multiple ESM-aware entry points
(`zustand`, `zustand/vanilla`, `zustand/middleware`, `zustand/middleware/immer`,
`zustand/shallow`, `zustand/traditional`). Bundlers that respect the `exports`
field of `package.json` will pick the right file automatically.

### 2.1 Requirements

| Item | Minimum |
|---|---|
| Node.js | 12.20.0 |
| React (peer, optional) | 18.0.0 |
| `@types/react` (peer, optional) | 18.0.0 |
| `use-sync-external-store` (peer, optional) | 1.2.0 |
| `immer` (peer, only if using the immer middleware) | 9.0.6 |
| TypeScript (if used) | 4.5 |

> [!IMPORTANT]
> All peer dependencies are marked optional. You only pay for what you import.
> A vanilla-only project that imports from `zustand/vanilla` does not need
> React installed.

### 2.2 Install methods

=== "npm"
    ```bash
    npm install zustand
    ```

=== "pnpm"
    ```bash
    pnpm add zustand
    ```

=== "yarn"
    ```bash
    yarn add zustand
    ```

=== "bun"
    ```bash
    bun add zustand
    ```

### 2.3 Verify the install

Drop the snippet below into a new file (`smoke.ts` or `smoke.js`) and run it
with `tsx`, `ts-node`, or `node` (after compiling to JS):

```ts
import { createStore } from 'zustand/vanilla'

const store = createStore<{ count: number }>(() => ({ count: 0 }))
store.subscribe((next) => console.log('count is', next.count))
store.setState({ count: 1 })
store.setState({ count: 2 })
```

Expected output:

```text
count is 1
count is 2
```

If those two lines appear, the install is healthy. The vanilla path has zero
React surface, so this works in Node, Deno, edge runtimes, and tests.

> [!NOTE]
> The library is published as a single package; there are no per-feature
> sub-packages to install. `zustand/middleware` is just a file inside the
> same npm tarball.

### 2.4 Project setup checklist

- [x] `zustand` is added to `dependencies` (not `devDependencies`).
- [x] React (or your framework) is already installed.
- [x] You set `"strict": true` in `tsconfig.json` if using TypeScript — the
      library's mutator types depend on strict inference to compose cleanly.
- [ ] Decide where store files live. A common layout:
      `src/stores/useUserStore.ts`, `src/stores/useCartStore.ts`.
- [ ] Decide on a slice/middleware strategy before the store grows past 50 lines.

## 3. Configuration

Zustand has no configuration file, no environment variables, and no global
toggles. Everything is configured in code by composing middleware at store
creation time. The only "config" surface is per-store options for individual
middleware (for example, `persist`'s `name` and `version`).

> [!TIP]
> If you find yourself wanting global config, you almost always want a slice
> instead. See the Slices Pattern section.

The settings most users touch are:

| Setting | Where it lives | Default |
|---|---|---|
| Persist storage backend | `persist(..., { storage })` | `createJSONStorage(() => window.localStorage)` |
| Persist storage key | `persist(..., { name })` | required |
| Devtools store name | `devtools(..., { name })` | none (anonymous in DevTools panel) |
| Devtools enabled | `devtools(..., { enabled })` | `import.meta.env?.MODE !== 'production'` |
| Subscribe equality function | `subscribe(selector, listener, { equalityFn })` | `Object.is` |

For the full per-middleware option set, see the Admin Guide section
"Configuration Reference".

## 4. Core Usage

This section covers the everyday API: creating a store, reading state with
selectors, writing actions, async actions, organizing with slices, and the
React-vs-vanilla split.

### 4.1 Create a store

`create` is the React-bound entry point. It accepts a *state creator* — a
function that receives `(set, get, api)` and returns the initial state object.

```tsx
import { create } from 'zustand'

type BearState = {
  bears: number
  increase: (by: number) => void
  reset: () => void
}

const useBearStore = create<BearState>((set) => ({
  bears: 0,
  increase: (by) => set((state) => ({ bears: state.bears + by })),
  reset: () => set({ bears: 0 }),
}))
```

The returned `useBearStore` is *both* a React hook and a `StoreApi` object.
That means `useBearStore.getState()`, `useBearStore.setState(...)`,
`useBearStore.subscribe(...)`, and `useBearStore.getInitialState()` are all
available outside React.

> [!NOTE]
> The two-step `create<State>()(initializer)` form (with the empty parens) is
> required when you stack middleware in TypeScript. The empty call helps
> TypeScript infer the mutator chain. If you do not use middleware, the
> single-call form `create<State>(initializer)` works.

### 4.2 Read state with selectors

A selector is a function `(state) => slice`. The hook re-runs the selector on
every state change and triggers a React render only if the new slice is not
`Object.is`-equal to the previous one.

```tsx
// Re-renders only when bears changes.
const bears = useBearStore((s) => s.bears)

// Re-renders only when the increase function reference changes (effectively never).
const increase = useBearStore((s) => s.increase)

// Reads the whole state. Re-renders on EVERY state change. Avoid in hot paths.
const everything = useBearStore()
```

**Selecting multiple values** has a subtlety: returning a new object on every
call defeats the equality check, because `{ a, b } !== { a, b }` by reference.
Use the `useShallow` helper from `zustand/shallow`:

```tsx
import { useShallow } from 'zustand/shallow'

const { bears, increase } = useBearStore(
  useShallow((s) => ({ bears: s.bears, increase: s.increase })),
)
```

`useShallow` memoizes the selector so it returns the same reference when each
key compares shallow-equal. Internally it stores the previous slice in a ref
and runs `shallow(prev, next)` from `zustand/vanilla/shallow`.

> [!WARNING]
> Do NOT inline a multi-key selector without `useShallow`. Code like
> `useBearStore((s) => ({ bears: s.bears, increase: s.increase }))` causes a
> re-render on every state change, even ones that don't touch those keys.

### 4.3 Write state with actions

Actions are just functions stored in state. They call `set` to update.

```tsx
type CartState = {
  items: string[]
  add: (item: string) => void
  remove: (item: string) => void
  clear: () => void
}

const useCart = create<CartState>((set, get) => ({
  items: [],
  add: (item) => set((state) => ({ items: [...state.items, item] })),
  remove: (item) =>
    set((state) => ({ items: state.items.filter((i) => i !== item) })),
  clear: () => set({ items: [] }),
}))
```

Two forms of `set`:

| Form | Behavior |
|---|---|
| `set({ items: [...] })` | Object form — shallow-merges with previous state |
| `set((state) => ({ items: [...] }))` | Function form — receives previous state, returns partial |
| `set(state, true)` | Replace mode — REPLACES state entirely (no merge) |

The shallow-merge default means `set({ items: [...] })` keeps `add`, `remove`,
and `clear` intact even though they aren't included in the partial. To replace
the entire state (for example, to reset to an initial snapshot), pass `true`
as the second argument.

> [!NOTE]
> If `set` produces a new state value that is `Object.is`-equal to the
> current state, listeners are NOT called. Setting `NaN` to `NaN` is a no-op
> for subscribers. Setting state to `null` works for both object and
> primitive stores.

`get()` returns the current state without subscribing — useful inside actions
that need to read before they write:

```tsx
const useCounter = create<{ count: number; inc: () => void }>(
  (set, get) => ({
    count: 0,
    inc: () => set({ count: get().count + 1 }),
  }),
)
```

### 4.4 Async actions

Actions can be async. Just mark them `async` and `await` inside. State stays
consistent because each `set` is synchronous; only the orchestration is async.

```tsx
type AuthState = {
  user: { id: string; name: string } | null
  loading: boolean
  error: string | null
  login: (username: string, password: string) => Promise<void>
  logout: () => void
}

const useAuth = create<AuthState>((set) => ({
  user: null,
  loading: false,
  error: null,
  login: async (username, password) => {
    set({ loading: true, error: null })
    try {
      const res = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ username, password }),
      })
      if (!res.ok) throw new Error('Invalid credentials')
      const user = await res.json()
      set({ user, loading: false })
    } catch (e) {
      set({ loading: false, error: (e as Error).message })
    }
  },
  logout: () => set({ user: null }),
}))
```

> [!TIP]
> Always set `loading: false` in BOTH the success and error branches. A
> finally block also works. Forgetting this leaves the UI permanently in a
> spinning state.

### 4.5 Slices pattern

For larger stores, split state into focused slices and combine them. Each
slice is a `StateCreator` that returns its piece. Spreading the slices builds
the final state object.

```tsx
import { create, StateCreator } from 'zustand'

interface BearSlice {
  bears: number
  addBear: () => void
}

interface FishSlice {
  fishes: number
  addFish: () => void
}

const createBearSlice: StateCreator<BearSlice & FishSlice, [], [], BearSlice> =
  (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 })),
  })

const useBoundStore = create<BearSlice & FishSlice>()((...a) => ({
  ...createBearSlice(...a),
  ...createFishSlice(...a),
}))
```

Each slice declares the *full* combined state in its first generic parameter —
that's how an action in `BearSlice` can call `set` to update fields owned by
`FishSlice` (for example, eating a fish to gain bears).

The fourth generic (`BearSlice` / `FishSlice` in the examples above) tells
TypeScript what subset of the combined state this slice contributes. The
type-test suite shows this pattern composing cleanly with the immer
middleware too.

### 4.6 React vs vanilla

Zustand has two entry points:

| Entry | API | When to use |
|---|---|---|
| `zustand` | `create`, `useStore` | React components |
| `zustand/vanilla` | `createStore`, `useStore` | Node, edge runtimes, tests, non-React frameworks |

`createStore` returns a plain `StoreApi`. There is no hook — you `subscribe`
manually:

```ts
import { createStore } from 'zustand/vanilla'

type Counter = { count: number; inc: () => void }

const counter = createStore<Counter>((set) => ({
  count: 0,
  inc: () => set((s) => ({ count: s.count + 1 })),
}))

const unsubscribe = counter.subscribe((state, prev) => {
  console.log(`count: ${prev.count} → ${state.count}`)
})

counter.getState().inc() // logs: count: 0 → 1
unsubscribe()
```

You can also bridge a vanilla store into React with `useStore` from `zustand`:

```tsx
import { useStore } from 'zustand'
const count = useStore(counter, (s) => s.count)
```

This is the pattern the library uses internally — `create` itself wraps a
`createStore` call and binds it to React.

### 4.7 Equality functions and `useStoreWithEqualityFn`

The default equality is `Object.is`. For values that need a different check
(for example, comparing arrays or objects shallowly without `useShallow`),
use the `traditional` entry point:

```tsx
import { createWithEqualityFn } from 'zustand/traditional'
import { shallow } from 'zustand/shallow'

const useStore = createWithEqualityFn<MyState>(
  (set) => ({ /* ... */ }),
  shallow, // default equality for all selectors of this store
)
```

`createWithEqualityFn` uses `useSyncExternalStoreWithSelector` from
`use-sync-external-store/shim/with-selector`, which accepts a selector and
equality function in one call. The selector is run each render, and the
equality function decides whether to bail out.

> [!NOTE]
> `createWithEqualityFn` exists for backward compatibility with Zustand v3's
> equality semantics and for users who prefer per-selector equality without
> wrapping every selector in `useShallow`. New code can use either.

### 4.8 The `StoreApi` surface

Whether created with `create` or `createStore`, every store exposes:

| Method | Type | Purpose |
|---|---|---|
| `getState()` | `() => T` | Current state snapshot |
| `getInitialState()` | `() => T` | Initial state (set once at creation) |
| `setState(partial, replace?)` | `(partial \| (state) => partial, boolean?) => void` | Update state |
| `subscribe(listener)` | `(listener) => unsubscribe` | Listen to all state changes |

The React hook adds nothing to that surface — it just wraps `subscribe` and
`getState` in `useSyncExternalStore`. You can call `useBearStore.getState()`
directly inside event handlers, async callbacks, or non-React code.

## 5. Common Tasks

This section walks through 10 real-world recipes pulled from idiomatic
Zustand usage.

### 5.1 Add a persist middleware (localStorage)

**Goal**: keep the cart in `localStorage` so it survives a page reload.

```tsx
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'

type CartState = {
  items: string[]
  add: (item: string) => void
}

const useCart = create<CartState>()(
  persist(
    (set) => ({
      items: [],
      add: (item) => set((s) => ({ items: [...s.items, item] })),
    }),
    {
      name: 'cart-storage', // localStorage key
      storage: createJSONStorage(() => localStorage),
    },
  ),
)
```

**Verify**: open DevTools, then Application, then Local Storage. After calling
`useCart.getState().add('apple')` you'll see a `cart-storage` key with
`{"state":{"items":["apple"]},"version":0}`.

> [!TIP]
> The `storage` option defaults to `createJSONStorage(() => window.localStorage)`,
> so for the simplest case you can omit it: `persist(initializer, { name: 'cart' })`.

### 5.2 Persist only part of state

**Goal**: keep `theme` and `locale` in localStorage but never persist `cart`.

```tsx
const useUI = create<UIState>()(
  persist(
    (set) => ({ theme: 'dark', locale: 'en', cart: [] }),
    {
      name: 'ui',
      partialize: (state) => ({ theme: state.theme, locale: state.locale }),
    },
  ),
)
```

`partialize` runs once per write and returns the slice of state that should
be serialized. Anything it omits stays in memory but never touches storage.

### 5.3 Persist with versioning and migration

**Goal**: rename `username` to `displayName` in v2 of your stored shape
without losing old data.

```tsx
const useAccount = create<AccountState>()(
  persist(
    (set) => ({ displayName: '', email: '' }),
    {
      name: 'account',
      version: 2,
      migrate: (persistedState: any, version) => {
        if (version === 1) {
          return { ...persistedState, displayName: persistedState.username }
        }
        return persistedState
      },
    },
  ),
)
```

When the stored version differs from the current `version`, Zustand calls
`migrate(state, oldVersion)` and uses its return value. If you bump version
without supplying `migrate`, Zustand prints `State loaded from storage
couldn't be migrated since no migrate function was provided` to the console
and falls back to initial state.

### 5.4 Add Redux DevTools

**Goal**: see every state transition in the Redux DevTools browser extension.

```tsx
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'

const useStore = create<MyState>()(
  devtools(
    (set) => ({
      count: 0,
      inc: () => set((s) => ({ count: s.count + 1 }), false, 'inc'),
      reset: () => set({ count: 0 }, false, 'reset'),
    }),
    { name: 'my-store' },
  ),
)
```

Pass an `action` name as the third argument to `set` (after `replace`) to
label the entry in DevTools. Without it, the entry is labeled by the
`anonymousActionType` option (default `'anonymous'`) or, in some browsers,
parsed from the call stack.

> [!IMPORTANT]
> Install the Redux DevTools browser extension first
> ([Chrome](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd)
> or [Firefox](https://addons.mozilla.org/en-US/firefox/addon/reduxdevtools/)).
> Without the extension, the middleware no-ops silently — no error.

### 5.5 Multiple stores in one DevTools panel

If you have two stores, the `store` option groups them under one connection
name:

```tsx
const useUser = create<UserState>()(
  devtools(initUser, { name: 'app', store: 'user' }),
)
const useCart = create<CartState>()(
  devtools(initCart, { name: 'app', store: 'cart' }),
)
```

DevTools will show both stores as a single tree keyed by `user` and `cart`,
and time-travel works across them via the `store` key in payloads.

### 5.6 Use Immer for nested updates

**Goal**: mutate deeply nested state without spreading.

```tsx
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'

type State = {
  user: { profile: { name: string; age: number } }
  setName: (name: string) => void
}

const useStore = create<State>()(
  immer((set) => ({
    user: { profile: { name: '', age: 0 } },
    setName: (name) =>
      set((state) => {
        state.user.profile.name = name // direct "mutation" — Immer makes it safe
      }),
  })),
)
```

Behind the scenes, the immer middleware passes your function-form `set`
through `produce` from the `immer` package, which captures the mutations and
applies them immutably.

> [!WARNING]
> The `immer` package is a peer dependency. Run `npm install immer` before
> importing from `zustand/middleware/immer`. The peerDependencies list
> requires `immer >= 9.0.6`.

### 5.7 Subscribe with a selector outside React

**Goal**: react to changes in `state.items.length` from a non-React module.

```tsx
import { create } from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'

const useCart = create<CartState>()(
  subscribeWithSelector((set) => ({ items: [], add: (i) => set((s) => ({ items: [...s.items, i] })) })),
)

const unsubscribe = useCart.subscribe(
  (state) => state.items.length,
  (count, prevCount) => console.log(`items: ${prevCount} → ${count}`),
  { equalityFn: Object.is, fireImmediately: true },
)
```

Without `subscribeWithSelector`, `subscribe` only takes a one-argument
listener that fires on every state change. The middleware overloads
`subscribe` to take a selector + listener + options and only fires when the
selector's output changes.

### 5.8 Use a Redux-style reducer

**Goal**: dispatch typed actions instead of calling functions.

```tsx
import { create } from 'zustand'
import { redux } from 'zustand/middleware'

type Action = { type: 'INC' } | { type: 'DEC' }

const useStore = create(
  redux(
    (state: { count: number }, action: Action) => {
      switch (action.type) {
        case 'INC': return { ...state, count: state.count + 1 }
        case 'DEC': return { ...state, count: state.count - 1 }
        default: return state
      }
    },
    { count: 0 },
  ),
)

// In a component:
const count = useStore((s) => s.count)
useStore.dispatch({ type: 'INC' })
```

The `redux` middleware adds a `dispatch(action)` method that runs your
reducer and `set`s the result. It also flips `dispatchFromDevtools = true`,
which makes DevTools (when stacked with `devtools`) able to dispatch actions
back into the store via the panel's "Dispatcher" tab.

### 5.9 Use `combine` to drop the type annotation

**Goal**: avoid writing the state interface twice.

```tsx
import { create } from 'zustand'
import { combine } from 'zustand/middleware'

const useStore = create(
  combine(
    { count: 0, name: '' },
    (set) => ({
      inc: () => set((s) => ({ count: s.count + 1 })),
      setName: (name: string) => set({ name }),
    }),
  ),
)
// useStore type is inferred as { count: number; name: string; inc: () => void; setName: (name: string) => void }
```

`combine(initialState, secondCreator)` returns a `StateCreator` that produces
`{ ...initialState, ...secondCreator(set, get, api) }`. TypeScript infers the
final shape from the union, so you don't repeat your fields in a type alias.

### 5.10 Reset the store to initial state

There is no built-in `reset()`. Capture the initial state and restore it:

```tsx
const initialState = { bears: 0, fish: 0 }

const useStore = create<State>((set) => ({
  ...initialState,
  reset: () => set(initialState, true), // replace mode
  // ... other actions
}))
```

The `replace = true` argument is critical: without it, your action functions
would be wiped out by the merge. With it, the entire state is replaced — but
because actions are part of `initialState`, they survive.

> [!TIP]
> If you have multiple stores that need a global reset (for example, on
> logout), keep the initial-state objects in module scope and call each
> store's reset action from a single function.

## 6. Troubleshooting

### Error: `Cannot set state of Zustand store in SSR`

**Cause**: You wrapped your store with `unstable_ssrSafe` and an action that
calls `set` ran on the server (where `typeof window === 'undefined'`). The
SSR-safe middleware throws on every server-side write because writes during
hydration cause client/server mismatch.

**Solution**:

1. Move the offending `set` into a `useEffect` so it only runs on the client.
2. If the write is intentionally server-side (rare), don't use `ssrSafe`.
3. Audit whether the write should be in the store at all — initial fetches
   often belong in a server component or loader, not in client state.

**Prevention**: only wrap with `unstable_ssrSafe` if you have a documented
SSR mismatch. For most React 18 apps, the default behavior is fine because
`useSyncExternalStore` already returns the initial state on the server.

### Re-renders happen on every state change, not just my slice

**Cause**: The selector returns a new object/array reference each call.

**Solution**: use `useShallow`:

```tsx
import { useShallow } from 'zustand/shallow'
const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })))
```

Or select primitives one at a time:

```tsx
const a = useStore((s) => s.a)
const b = useStore((s) => s.b)
```

**Prevention**: never inline a selector that returns a new object literal
without `useShallow`.

### Persisted state never appears

**Cause**: storage is undefined when the store initializes (SSR or
no-window environments). Zustand prints `Unable to update item '<name>',
the given storage is currently unavailable.` and falls back to in-memory
state.

**Solution**: gate `createJSONStorage` on the existence of `window`:

```tsx
storage: createJSONStorage(() =>
  typeof window !== 'undefined' ? localStorage : (undefined as any),
)
```

The `createJSONStorage` factory catches the throw and returns `undefined`,
which the `persist` middleware then treats as "no storage".

**Prevention**: in Next.js, set `skipHydration: true` and call
`useStore.persist.rehydrate()` from a `useEffect`. See
the Admin Guide section "Server-Side Rendering" for the full pattern.

### Stack overflow in `setState` after enabling devtools

**Cause**: A `set` call inside a subscribe listener triggers another
DevTools `send`, which (in some configurations) re-enters via a JUMP_TO
message.

**Solution**: don't call `setState` from inside a `subscribe` listener.
Use `subscribeWithSelector` so listeners only fire on selected changes,
and route writes through actions on the store, not from the listener.

**Prevention**: keep listeners side-effect-free; if you must mutate, use
`queueMicrotask(() => useStore.setState(...))` to break the synchronous loop.

### TypeScript: "Type instantiation is excessively deep"

**Cause**: deeply nested middleware stack confuses TS's recursive mutator
type. Often appears with 3+ middleware on a strict config.

**Solution**: use the curried form to give TypeScript the state type up
front:

```tsx
const useStore = create<MyState>()(
  devtools(persist(immer(initializer), { name: 'x' }), { name: 'y' }),
)
```

Or annotate the mutator chain explicitly:

```tsx
const useStore = create<
  MyState,
  [
    ['zustand/devtools', never],
    ['zustand/persist', MyState],
    ['zustand/immer', never],
  ]
>(devtools(persist(immer(...), {name:'x'}), {name:'y'}))
```

**Prevention**: keep middleware stacks shallow; consolidate persistence,
devtools, and immer into a single `combine`-style helper if you reuse them
across many stores.

### Store updates don't trigger React re-render in tests

**Cause**: Tests using `react-test-renderer` or older test setups may not
flush `useSyncExternalStore` correctly.

**Solution**: use `@testing-library/react` (the same library Zustand's own
test suite uses, version `^16.3.2`). Wrap state-changing calls in `act`:

```tsx
import { act } from '@testing-library/react'
act(() => useStore.getState().increment())
```

**Prevention**: import from `@testing-library/react`, not the older
`react-test-renderer`. JSDOM 27+ is what the project's own vitest config
uses for component tests.

### `import.meta.env` is undefined in non-Vite bundlers (issue #3437)

**Cause**: the devtools middleware reads `import.meta.env?.MODE` to decide
whether to enable. Bundlers that don't polyfill `import.meta.env` (Expo
Metro, certain webpack configs) can throw at runtime.

**Solution**: pass `enabled` explicitly to bypass the check:

```tsx
devtools(initializer, {
  name: 'my-store',
  enabled: process.env.NODE_ENV !== 'production',
})
```

**Prevention**: in non-Vite environments, always pass `enabled` to
`devtools()`. The optional-chaining is defensive but not a polyfill.

> [!CAUTION]
> Issue #3437 (closed) tracked the original bug. If you upgrade an Expo or
> Metro project from an older Zustand and see a runtime
> `Cannot read properties of undefined (reading 'env')`, this is the cause.

## 7. FAQ

**Q: Do I need a Provider component?**

No. Zustand stores are module-scoped singletons. The hook reaches into the
store directly via `useSyncExternalStore`. If you need request-scoped state
(SSR, micro-frontends), create the store inside a React Context — see the
Admin Guide section "Server-Side Rendering" for the recommended pattern.

**Q: How do I share state between two stores?**

Don't. Compose them with the slices pattern (Section 4.5) or read across
stores via `getState()` inside an action:

```tsx
const useA = create((set) => ({ /* ... */ }))
const useB = create((set) => ({
  doThing: () => {
    const aValue = useA.getState().value
    set({ result: aValue * 2 })
  },
}))
```

Cross-store subscriptions also work via `useA.subscribe(...)` from inside a
`createStore` call's body or a module-level effect.

**Q: When should I use `useShallow` vs `createWithEqualityFn`?**

Use `useShallow` for one-off shallow selections. Use `createWithEqualityFn`
when you want every selector on a store to default to a non-`Object.is`
equality. Most apps stick with `create` plus `useShallow` per call site.

**Q: Can I use Zustand without React?**

Yes — import from `zustand/vanilla`. The library was built so the React
adapter is just a thin wrapper. Vue, Svelte, lit-html, plain DOM apps, and
Node services all use the vanilla path.

**Q: Does Zustand support React Server Components?**

Stores are client-side. Mark files that import `zustand` with the
`'use client'` directive in Next.js App Router. To pass server-fetched data
into a client store, use the props-as-initial-state pattern: hoist the
store creation into a Context Provider that takes initial state from props.

**Q: What about React Native?**

Same package. The `package.json` `exports` field has a `react-native`
condition that resolves to the same files as web. For persistence, use
`@react-native-async-storage/async-storage` as your storage backend:

```tsx
import AsyncStorage from '@react-native-async-storage/async-storage'
persist(initializer, {
  name: 'app',
  storage: createJSONStorage(() => AsyncStorage),
})
```

`AsyncStorage` returns Promises; `createJSONStorage` and `persist` handle
async storage transparently.

**Q: How do I time-travel with DevTools?**

Wrap your store with `devtools` (Section 5.4), open the Redux DevTools
extension, and use the slider at the bottom of the panel. JUMP_TO_STATE,
JUMP_TO_ACTION, COMMIT, RESET, ROLLBACK, IMPORT_STATE, and PAUSE_RECORDING
are all supported by the middleware.

**Q: How do I unsubscribe?**

`subscribe` returns the unsubscribe function. Call it when you're done:

```tsx
const unsub = useStore.subscribe((state) => { /* ... */ })
// later:
unsub()
```

In React components, call `subscribe` from a `useEffect` and return the
unsubscribe from the cleanup callback.

**Q: Is the store mutable? Can I just do `state.x = 1`?**

No. Always go through `set`. Direct mutation skips the listener notification
and your components won't re-render. If you want mutation-style writes, use
the `immer` middleware (Section 5.6).

**Q: How do I test a store?**

Import the store, call `.getState()` to read, `.setState(...)` to seed,
`.subscribe(...)` to spy on changes, and reset between tests by capturing
the initial state and calling `setState(initial, true)`. The project's own
`tests/vanilla/basic.test.ts` is a tight reference.

**Q: Where do I report bugs?**

[github.com/pmndrs/zustand/issues](https://github.com/pmndrs/zustand/issues).
Real-world referenced issues in this guide include #100 (sentiment),
#2659 (help wanted) and #3437 (the `import.meta.env` Metro bundler bug).

---

_Generated April 2026 from pmndrs/zustand at 3201328. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
