# Update nested state with Immer

Use [`create`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand#create) with [`immer`](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/zustand-middleware-immer#immer) when an action changes several levels of an object or a collection. You write the update with draft-mutation syntax; the middleware produces the immutable state that Zustand passes to its subscribers.

## When to use it

Use Immer when spreading every level of a nested value makes an action hard to read. Zustand's normal update is a shallow merge, so replacing a nested object without copying its existing properties can discard sibling fields. For a single flat field, a regular `set` update remains enough.

Install both packages in your application:

```bash
npm install zustand immer
```

## Update nested objects and a collection

Define the state and actions together. The `immer` wrapper changes the `set` callback so its `state` argument is an Immer draft.

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

type Profile = {
  name: string
  preferences: {
    theme: 'light' | 'dark'
    notifications: boolean
  }
}

type Todo = {
  id: string
  title: string
  done: boolean
}

type Store = {
  profile: Profile
  todos: Todo[]
  setTheme: (theme: Profile['preferences']['theme']) => void
  toggleTodo: (id: string) => void
}

export const useStore = create<Store>()(
  immer((set) => ({
    profile: {
      name: 'Ada Lovelace',
      preferences: {
        theme: 'light',
        notifications: true,
      },
    },
    todos: [
      { id: 'learn-zustand', title: 'Learn Zustand', done: false },
      { id: 'write-example', title: 'Write an example', done: false },
    ],
    setTheme: (theme) =>
      set((state) => {
        state.profile.preferences.theme = theme
      }),
    toggleTodo: (id) =>
      set((state) => {
        const todo = state.todos.find((item) => item.id === id)
        if (todo) {
          todo.done = !todo.done
        }
      }),
  })),
)

export function App() {
  const name = useStore((state) => state.profile.name)
  const theme = useStore((state) => state.profile.preferences.theme)
  const todos = useStore((state) => state.todos)
  const setTheme = useStore((state) => state.setTheme)
  const toggleTodo = useStore((state) => state.toggleTodo)

  return (
    <main>
      <h1>{name}</h1>
      <p>Theme: {theme}</p>
      <button type="button" onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle theme
      </button>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <button type="button" onClick={() => toggleTodo(todo.id)}>
              {todo.done ? 'Done' : 'Open'}: {todo.title}
            </button>
          </li>
        ))}
      </ul>
    </main>
  )
}
```

![The rendered profile name, theme control, and two todo items appear.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/b595adc48d78741c1628b36d069e89c1.png)

The mounted component displays the profile name, the current theme, and both todos. Clicking **Toggle theme** changes only `profile.preferences.theme`; the other profile fields remain present. Clicking a todo changes that item's `done` value and updates its label from `Open` to `Done` (or back). The selectors subscribe to the values they read, so the component receives the produced state through the store hook.

## Keep collection references observable

Do not mutate a `Map` or `Set` in place in a regular Zustand update. An in-place mutation keeps the same collection reference, so Zustand may treat the selected value as unchanged. Create a new `Map` or `Set` for each update instead. The array update above uses Immer draft syntax, which produces the immutable collection state for the changed item.

## Watch the Immer edge cases

Immer must proxy the value you mutate. If you mutate a class object without marking it `[immerable] = true`, Immer can change the current object without producing a new proxied state. Zustand can then see equal current and next state and skip subscriptions. Mark such class objects as Immerable and follow Immer's rules.

## See it running

Open the [nested-state demo](https://stackblitz.com/edit/vitejs-vite-j6bjdygu) to see the rendered result in a browser. For the middleware's standalone examples, see the [basic](https://stackblitz.com/edit/vitejs-vite-3sgc4ejy) and [advanced](https://stackblitz.com/edit/vitejs-vite-jxxtuyj3) demos.

For selector stability and shallow comparison, continue with [Selectors and rendering](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/selectors-and-rendering). For a vanilla store, see [Create a vanilla store](https://bench-zustand-56.atloria.app/p/bench-zustand-56-D8Xr4XerWj/developer/create-a-vanilla-store).
