Skip to content
D
Documentation

Update nested state

how-to
2 min readUpdated

Use immutable spreads for a small nested update; use the immer middleware when copying every level obscures the change. Both approaches keep actions beside state in a store created with 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.

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

ApproachWhat you writeUse it when
Immutable update with setReturn 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 middlewareWrap 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. For selector reference stability, see Select state efficiently.

Live demo

Try the authors' updating-state demo to see nested updates in a running React app.

Was this page helpful?

Update nested state — zustand · GPT-6 Luna