# How to Use Immer with Zustand Middleware for Immutable State Updates

> Learn how to use Immer with Zustand middleware for immutable state updates. Safely mutate state with Immer's produce method in your Zustand store actions.

- Repository: [Poimandres/zustand](https://github.com/pmndrs/zustand)
- Tags: tutorial
- Published: 2026-03-06

---

**Zustand's `immer` middleware enables "mutative" draft functions in your store actions by wrapping `setState` with Immer's `produce` method, automatically generating immutable updates while preserving reference equality and change detection.**

The pmndrs/zustand repository ships a dedicated `immer` middleware that eliminates the need for manual spread operators when updating nested objects. By intercepting the `set` function in [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts) and processing functional updaters through Immer, this middleware lets you write direct property assignments that compile to immutable state transitions. This guide explains the internal architecture defined in [`src/middleware/immer.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/immer.ts) and demonstrates proper integration patterns for vanilla and React environments.

## Architecture of the Immer Middleware

Zustand's middleware system relies on higher-order functions that transform the state creator before it reaches the core store implementation.

### The State Creator Pattern

At the lowest level, Zustand stores are initialized by a creator function that receives the raw `setState`, `getState`, and the store API:

```typescript
(set, get, store) => ({ /* state & actions */ })

```

This initializer is processed by `createStore` in [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts) (lines 66-84), which provides the base `setState` implementation that the middleware will decorate.

### The immerImpl Wrapper

The `immer` middleware, defined in [`src/middleware/immer.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/immer.ts) (lines 5-88), exports an `immerImpl` function that patches the store's `setState` method:

```typescript
const immerImpl: ImmerImpl = (initializer) => (set, get, store) => {
  store.setState = (updater, replace, ...args) => {
    const nextState = typeof updater === 'function'
      ? produce(updater as any)               // ← Immer creates a draft
      : updater
    return set(nextState, replace as any, ...args)
  }
  return initializer(store.setState, get, store)
}

```

When your action calls `set(draft => { draft.count++ })`, the middleware detects that the updater is a function, passes it to `produce`, and forwards the resulting immutable state to Zustand's core setter. Plain values (non-functions) pass through unchanged.

### TypeScript Augmentation

The same file (lines 21-68) provides `WithImmer` and `StoreImmer` utility types that augment the store's `setState` signature. This allows TypeScript to recognize when you're passing a draft-mutating function versus a partial state object, enabling full type safety for nested mutations.

## Implementing Immer in Your Store

### Basic Vanilla Store Setup

For non-React environments, import `createStore` from the vanilla entry point and wrap your initializer with `immer`:

```tsx
import { createStore } from 'zustand/vanilla'
import { immer } from 'zustand/middleware/immer'

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

export const counterStore = createStore<CounterState>()(
  immer((set) => ({
    count: 0,
    inc: () => set((state) => { state.count += 1 }),
    dec: () => set((state) => { state.count -= 1 }),
    add: (n) => set((state) => { state.count += n })
  }))
)

```

Note that `set` now accepts a function receiving a mutable draft of your state.

### React Integration with Devtools

When using React, compose `immer` with other middlewares like `devtools`. According to the advanced TypeScript guide in [`docs/learn/guides/advanced-typescript.md`](https://github.com/pmndrs/zustand/blob/main/docs/learn/guides/advanced-typescript.md) (line 226), place `immer` closest to the store to prevent other middlewares from overwriting the patched `setState`:

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

type Todo = { id: number; text: string; done: boolean }

type TodoStore = {
  todos: Todo[]
  add: (text: string) => void
  toggle: (id: number) => void
}

export const useTodoStore = create<TodoStore>()(
  devtools(
    immer((set) => ({
      todos: [],
      add: (text) =>
        set((state) => {
          state.todos.push({ id: Date.now(), text, done: false })
        }),
      toggle: (id) =>
        set((state) => {
          const todo = state.todos.find((t) => t.id === id)
          if (todo) todo.done = !todo.done
        })
    }))
  )
)

```

### Persistence with Immer

The `immer` middleware composes cleanly with `persist` for state hydration. Wrap `immer` inside `persist` to ensure mutations are processed before serialization:

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

type ThemeStore = {
  dark: boolean
  toggle: () => void
}

export const useThemeStore = create<ThemeStore>()(
  persist(
    immer((set) => ({
      dark: false,
      toggle: () => set((state) => { state.dark = !state.dark })
    })),
    { name: 'theme-storage' }
  )
)

```

### TypeScript Configuration

For strict typing, the middleware augments the `StoreMutators` interface. The generated `setState` signature accepts either a partial state object or a draft mutator:

```typescript
import { StoreApi } from 'zustand/vanilla'
import { immer } from 'zustand/middleware/immer'

// The immer middleware automatically updates the setState type to:
type ImmerSetState<T> = (
  updater: ((draft: T) => void) | Partial<T>,
  replace?: boolean,
  ...args: any[]
) => void

```

This type inference is handled internally by the `WithImmer` type definition in [`src/middleware/immer.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/immer.ts).

## Best Practices for Middleware Ordering

Middleware wrapping order determines which layer intercepts `setState` first. Since `immer` patches `store.setState` directly, wrapping it with `devtools` (as `devtools(immer(...))`) ensures that:

1. Immer processes the draft mutation first
2. The resulting immutable state flows outward to devtools for time-travel debugging
3. Subsequent middlewares receive the finalized state object

Placing `immer` as the innermost wrapper guarantees that the draft-to-immutable conversion happens before any logging or persistence layer examines the update.

## Summary

- **Core mechanism**: The `immer` middleware in [`src/middleware/immer.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/immer.ts) intercepts `setState` and runs functional updaters through Immer's `produce` method.
- **Syntax**: Write `set(draft => { draft.property = value })` instead of manual object spreading.
- **Composition**: Wrap `immer` closest to the store creator when chaining with `devtools` or `persist` to preserve the draft functionality.
- **Type safety**: The middleware augments TypeScript definitions to recognize draft functions as valid setters.

## Frequently Asked Questions

### What is the correct way to update arrays with Zustand Immer middleware?

Use standard mutable array methods like `push`, `splice`, or direct index assignment inside the draft function. The middleware translates these mutations into immutable updates. For example: `set(state => { state.items.push(newItem) })` safely appends to your array without manual copying.

### Can I use the Immer middleware with the vanilla `createStore` API?

Yes. Import `createStore` from `zustand/vanilla` and apply `immer` exactly as you would with the React `create` function. The middleware operates at the store level, independent of UI bindings, making it compatible with any Zustand consumer including Vue, Svelte, or Node.js scripts.

### Why does middleware order matter when using `devtools` with `immer`?

The `immer` middleware replaces the `store.setState` reference with a wrapper that calls `produce`. If you wrap `devtools` inside `immer` (i.e., `immer(devtools(...))`), the devtools layer replaces `setState` after Immer has patched it, potentially stripping the draft-processing behavior. Always use `devtools(immer(...))` to ensure Immer's wrapper remains active.

### Does the Immer middleware handle non-function set calls?

Yes. The wrapper in [`src/middleware/immer.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/immer.ts) checks `typeof updater === 'function'`. If you pass a plain object like `set({ count: 5 })`, it bypasses `produce` and forwards the value directly to the core `setState`, maintaining backward compatibility with standard Zustand usage patterns.