What does the immer middleware do?
immer is one of the most useful middlewares in Zustand, and it makes working with state simpler, safer, and more convenient, especially if you don't want to manually track immutability.
The essence in one sentence
The
immer-middleware lets you change state "as if directly", but under the hood Zustand still creates an immutable copy (it does not mutate the original).
Without immer
In plain Zustand you have to explicitly return a new object so React understands that the state changed:
import { create } from 'zustand'
const useTodoStore = create((set) => ({
todos: [],
addTodo: (text) =>
set((state) => ({
todos: [...state.todos, { text, done: false }],
})),
}))Here it's important not to mutate state.todos, otherwise React won't see the change.
That is, you cannot do state.todos.push(...).
With immer
With immer you can write code in a mutable style, and the library creates an immutable copy under the hood:
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
const useTodoStore = create(
immer((set) => ({
todos: [],
addTodo: (text) =>
set((state) => {
state.todos.push({ text, done: false }) // mutation is fine!
}),
toggleTodo: (index) =>
set((state) => {
state.todos[index].done = !state.todos[index].done
}),
}))
)Now Zustand automatically:
- creates a "draft" of the state (
draft); - applies your changes to it;
- creates a new version of the state with no mutations.
How this works under the hood
immer uses the Immer library, which implements the immutable state via Proxy concept.
Roughly like this:
- Zustand creates a proxy object (
draft) of the state; - You "mutate" it like a regular object;
- Immer records the changes and creates a new immutable copy of the state.
Advantages
| Advantage | Description |
|---|---|
| Clean code | You write in a natural style without copying arrays and objects |
| Safety | Guarantees immutability (the original is not mutated) |
| Simplicity | No need to manually use spreads (...) |
| Compatibility | Works with persist, devtools, and other middleware |
Important to remember
- Immer is a bit slower than "pure" immutable updates if updates are very frequent (for example, 60 times per second in an animation). But in typical applications the difference is unnoticeable.
- Immer cannot be used together with direct mutations outside
set()- it only works inside theset((state) => { ... })function.
Summary
| Parameter | Without immer | With immer |
|---|---|---|
| Updating an array | set({ todos: [...state.todos, newTodo] }) | set((s) => { s.todos.push(newTodo) }) |
| Immutability | manual | automatic |
| Code | verbose | concise |
| Safety | depends on the developer | guaranteed by Immer |
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.