Skip to main content
The immer middleware enables you to write state updates using mutable syntax while maintaining immutability under the hood. This eliminates boilerplate code when updating nested objects and arrays.
You must install immer as a dependency to use this middleware:
npm install immer
import { immer } from 'zustand/middleware/immer'

const useStore = create(
  immer((set) => ({
    person: { name: 'John', age: 30 },
    updateName: (name: string) =>
      set((state) => {
        state.person.name = name
      }),
  }))
)

Signature

immer<T>(
  stateCreator: StateCreator<T, [], []>
): StateCreator<T, [['zustand/immer', never]], []>

Parameters

stateCreator
StateCreator<T>
required
The state creator function that defines your store. The set function accepts Immer draft mutations.

Without Immer

Traditional immutable updates require spreading nested objects:
import { create } from 'zustand'

type PersonStore = {
  person: { firstName: string; lastName: string; email: string }
  setPerson: (updater: (person: PersonStore['person']) => PersonStore['person']) => void
}

const usePersonStore = create<PersonStore>()((set) => ({
  person: {
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: '[email protected]',
  },
  setPerson: (updater) =>
    set((state) => ({
      person: updater(state.person),
    })),
}))

// Usage requires spreading
function handleFirstNameChange(firstName: string) {
  usePersonStore.getState().setPerson((person) => ({
    ...person,
    firstName,
  }))
}

With Immer

Immer allows direct mutation of draft state:
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'

type PersonStore = {
  person: { firstName: string; lastName: string; email: string }
  updateFirstName: (firstName: string) => void
  updateLastName: (lastName: string) => void
  updateEmail: (email: string) => void
}

const usePersonStore = create<PersonStore>()(
  immer((set) => ({
    person: {
      firstName: 'Barbara',
      lastName: 'Hepworth',
      email: '[email protected]',
    },
    updateFirstName: (firstName) =>
      set((state) => {
        state.person.firstName = firstName
      }),
    updateLastName: (lastName) =>
      set((state) => {
        state.person.lastName = lastName
      }),
    updateEmail: (email) =>
      set((state) => {
        state.person.email = email
      }),
  }))
)

// Usage is cleaner
function handleFirstNameChange(firstName: string) {
  usePersonStore.getState().updateFirstName(firstName)
}

Nested Arrays

Immer shines when working with nested arrays:
type TodoStore = {
  todos: Array<{ id: string; text: string; completed: boolean }>
  toggleTodo: (id: string) => void
  addTodo: (text: string) => void
  removeTodo: (id: string) => void
}

const useTodoStore = create<TodoStore>()(
  immer((set) => ({
    todos: [],
    toggleTodo: (id) =>
      set((state) => {
        const todo = state.todos.find((t) => t.id === id)
        if (todo) {
          todo.completed = !todo.completed
        }
      }),
    addTodo: (text) =>
      set((state) => {
        state.todos.push({
          id: crypto.randomUUID(),
          text,
          completed: false,
        })
      }),
    removeTodo: (id) =>
      set((state) => {
        const index = state.todos.findIndex((t) => t.id === id)
        if (index !== -1) {
          state.todos.splice(index, 1)
        }
      }),
  }))
)

Deep Nested Updates

type Store = {
  user: {
    profile: {
      settings: {
        notifications: {
          email: boolean
          push: boolean
        }
      }
    }
  }
  toggleEmailNotifications: () => void
}

const useStore = create<Store>()(
  immer((set) => ({
    user: {
      profile: {
        settings: {
          notifications: {
            email: true,
            push: false,
          },
        },
      },
    },
    toggleEmailNotifications: () =>
      set((state) => {
        state.user.profile.settings.notifications.email =
          !state.user.profile.settings.notifications.email
      }),
  }))
)

Composing with Other Middleware

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

const useStore = create<Store>()(
  devtools(
    persist(
      immer((set) => ({
        count: 0,
        increment: () =>
          set((state) => {
            state.count += 1
          }),
      })),
      { name: 'counter-storage' }
    ),
    { name: 'CounterStore' }
  )
)

Type Safety

Immer preserves TypeScript types and provides autocomplete:
type Store = {
  data: {
    name: string
    count: number
  }
  update: () => void
}

const useStore = create<Store>()(
  immer((set) => ({
    data: { name: 'test', count: 0 },
    update: () =>
      set((state) => {
        // TypeScript knows state.data has name and count
        state.data.count += 1
        state.data.name = state.data.name.toUpperCase()
      }),
  }))
)

Returning Values

Immer mutations can return values:
const useStore = create<Store>()(
  immer((set) => ({
    items: [],
    addItem: (item: Item) =>
      set((state) => {
        state.items.push(item)
        return state // Optional: explicitly return state
      }),
    // Or return a completely new state
    reset: () =>
      set(() => ({
        items: [],
      })),
  }))
)

Build docs developers (and LLMs) love