How to Update Nested State with a Reducer

Update nested objects and arrays in a reducer immutably by copying each level on the path to the change, with an Immer shortcut.

5 min read

To update nested state with a reducer, copy every object from the changed field up to the top. The reducer returns a fresh object at each level instead of mutating the old one. Deep clones are not required; a shallow spread at each touched level is enough.

Getting the copies right prevents silent update bugs, where the user types a value and the screen never reflects it.

Why shallow copies are not enough

The spread operator copies only one level. If you spread the top object but leave a nested object untouched, that nested object still points at the previous render's data. React compares references, so an unchanged nested reference looks like no update.

A nested object is really a separate object, so updating it means building a new child and pointing the new parent at it.

index.jsindex.js
function reducer(person, action) {
  switch (action.type) {
    case "changed_city":
      return {
        ...person,
        address: { ...person.address, city: action.city }
      };
    default:
      return person;
  }
}

The changed_city case copies person, then copies address and overrides city. Both levels get new references, so React can see that the state changed. See how to update objects in React state for the same rule outside reducers.

Update an array nested in an object

When the nested value is an array, replace it with map or filter. Map returns a new array, and each changed item gets its own new object.

index.jsindex.js
function reducer(state, action) {
  switch (action.type) {
    case "toggled":
      return {
        ...state,
        tasks: state.tasks.map((task) => (task.id === action.id ? { ...task, done: !task.done } : task))
      };
    default:
      return state;
  }
}

The toggled case spreads state, then maps tasks into a new array. The matching task is copied with its done flag flipped, while the others keep their references. Nothing below the change is mutated, and each list item still needs a stable key when it is rendered.

Flatten when the nesting hurts

Deep nesting makes every update copy more levels. Before reaching for Immer, ask whether the nested data really needs to be nested. A flat shape, such as a separate task array instead of tasks inside a user object, updates with a single spread.

Use Immer when copying gets noisy

For deeply nested state, Immer lets you write mutating style while it produces the copies for you. Its useImmerReducer hook takes a reducer that can mutate a draft.

index.jsindex.js
import { useImmerReducer } from "use-immer";
function reducer(draft, action) {
  switch (action.type) {
    case "changed_city":
      draft.address.city = action.city;
      break;
    default:
      break;
  }
}

The draft is a safe copy that records your edits, so the reducer never mutates real state. Immer figures out which parts of the draft changed and builds the new object for you, so the code reads like mutation without being one.

Immer is a peer dependency, so install both with npm install immer use-immer, then swap useReducer for useImmerReducer. Read how to write actions and reducers for the purity rules Immer is respecting for you.

Rune AI

Rune AI

Key Insights

  • Spread copies only one level.
  • Copy each object on the path to the change.
  • Map and filter replace arrays immutably.
  • Immer lets you write mutating style safely.
  • Consider flattening deeply nested state.
RunePowered by Rune AI

Frequently Asked Questions

Why is the spread operator not enough for nested state?

Spread copies only one level deep. A nested object still points at the old object, so you must spread that level too.

Do I need to deep clone the whole state?

No. Copy only the objects on the path from the changed field up to the top level. Everything else can keep its old reference.

What if my state gets deeply nested?

Consider flattening the state shape. If you keep the nesting, Immer's useImmerReducer removes the repetitive copying.

Conclusion

Updating nested state in a reducer means copying every object from the changed field up to the root. Spread at each touched level, replace arrays with map and filter, and reach for Immer when the copying becomes repetitive.