# Update nested state

Use immutable spreads for a small nested update; use the [`immer`](https://bench-zustand-6l.atloria.app/p/bench-zustand-6l-TzFgfybqJX/developer/zustand-middleware-immer#immer) middleware when copying every level obscures the change. Both approaches keep actions beside state in a store created with [`create`](https://bench-zustand-6l.atloria.app/p/bench-zustand-6l-TzFgfybqJX/developer/zustand#create).

## Update each nested level with `set`

`set` merges only the top-level state. For nested data, return a new object at each level on the path to the changed value and spread each existing object to preserve its other fields.

The example mounts a counter with two buttons. The first uses explicit immutable copies; the second uses the Immer middleware's draft update. Either button increments the nested count shown on screen.

1. Install Zustand and Immer, which the middleware requires as a direct dependency:

   ```bash
   npm install zustand immer
   ```

2. Put this in your React entry file, such as `src/main.tsx`. The HTML page needs a `<div id="root"></div>` mount point.

   ```tsx
   import { createRoot } from 'react-dom/client'
   import { create } from 'zustand'
   import { immer } from 'zustand/middleware/immer'

   type State = {
     deep: {
       nested: {
         obj: { count: number }
       }
     }
   }

   type Actions = {
     incrementWithSpreads: () => void
     incrementWithImmer: () => void
   }

   const useCounterStore = create<State & Actions>()(
     immer((set) => ({
       deep: { nested: { obj: { count: 0 } } },
       incrementWithSpreads: () =>
         set((state) => ({
           deep: {
             ...state.deep,
             nested: {
               ...state.deep.nested,
               obj: {
                 ...state.deep.nested.obj,
                 count: state.deep.nested.obj.count + 1,
               },
             },
           },
         })),
       incrementWithImmer: () =>
         set((state) => {
           state.deep.nested.obj.count += 1
         }),
     })),
   )

   function NestedCounter() {
     const count = useCounterStore((state) => state.deep.nested.obj.count)
     const incrementWithSpreads = useCounterStore(
       (state) => state.incrementWithSpreads,
     )
     const incrementWithImmer = useCounterStore(
       (state) => state.incrementWithImmer,
     )

     return (
       <main>
         <p>Nested count: {count}</p>
         <button onClick={incrementWithSpreads}>Increment with spreads</button>
         <button onClick={incrementWithImmer}>Increment with Immer</button>
       </main>
     )
   }

   const container = document.getElementById('root')
   if (!container) {
     throw new Error('Missing #root mount point')
   }

   createRoot(container).render(<NestedCounter />)
   ```

![The rendered counter displays its nested count above the two increment buttons.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/719ee89c8549d128168c5e1e5e0f03f9.png)

After either button is clicked, the displayed nested count increases by one. The spread-based action explicitly copies `deep`, `nested`, and `obj`; the Immer action changes the draft at the target path. The component selects the count and each action, so a count change updates the rendered value.

## Choose the update style

| Approach | What you write | Use it when |
| --- | --- | --- |
| Immutable update with `set` | Return a new object for every nested level on the changed path, spreading each existing object. | The nested update is short and the copies make the change clear. |
| Immer middleware | Wrap the store initializer with `immer`, then mutate the draft in a `set` callback. | Explicit copies make a deeply nested update hard to read. |

The middleware lets the `set` callback use mutable syntax while Immer produces the immutable update. You do not need to call `produce` yourself for this pattern.

## Pitfall

Do not replace a nested object with only the changed field. Zustand's merge is shallow, so that replacement discards sibling fields in the nested object. Spread the existing nested value before overriding the field, as `incrementWithSpreads` does.

For class instances used with Immer, see [Immutable state updates](https://bench-zustand-6l.atloria.app/p/bench-zustand-6l-TzFgfybqJX/developer/immutable-state-updates). For selector reference stability, see [Select state efficiently](https://bench-zustand-6l.atloria.app/p/bench-zustand-6l-TzFgfybqJX/developer/select-state-efficiently).

## Live demo

Try the authors' [updating-state demo](https://stackblitz.com/edit/vitejs-vite-j6bjdygu) to see nested updates in a running React app.

## Related

- [React quick start](https://bench-zustand-6l.atloria.app/p/bench-zustand-6l-TzFgfybqJX/developer/react-quick-start)
- [Immutable state updates](https://bench-zustand-6l.atloria.app/p/bench-zustand-6l-TzFgfybqJX/developer/immutable-state-updates)
- [Immer middleware reference](https://bench-zustand-6l.atloria.app/p/bench-zustand-6l-TzFgfybqJX/developer/zustand-middleware-immer)
