Immutable Updates

Replace, Don't Mutate

Immutable Updates

Updating state means giving React a NEW object or array. Mutating the existing one in place won't trigger a re-render.

4 min read Level 2/5 #react#state#immutability
What you'll learn
  • Update arrays without mutation
  • Update nested objects without mutation
  • Recognize the mutate-and-set bug

React uses Object.is to decide whether state changed. If you mutate an existing object and pass it back, the reference is the same — React skips the re-render. You’ll see your state update in DevTools, but the UI won’t move.

The rule: always give the setter a NEW object or array.

Arrays — Replace, Don’t Mutate

Operation✗ Mutating✓ Immutable
Add to endarr.push(x)setArr([...arr, x])
Add to startarr.unshift(x)setArr([x, ...arr])
Remove by indexarr.splice(i, 1)setArr(arr.filter((_, idx) => idx !== i))
Remove by idsetArr(arr.filter(item => item.id !== id))
Update by idarr[i].x = …setArr(arr.map(item => item.id === id ? { ...item, x: … } : item))
Insert at indexarr.splice(i, 0, x)setArr([...arr.slice(0, i), x, ...arr.slice(i)])
Sortarr.sort()setArr([...arr].sort(...))

.map, .filter, .slice, .concat, spread — all return new arrays. .push, .pop, .splice, .sort, .reverse mutate in place. Stick to the new-array ones.

Objects — Spread, Don’t Assign

// ✗ Mutating — no re-render
user.name = "Ada";
setUser(user);

// ✓ New object
setUser({ ...user, name: "Ada" });

Nested Objects — Spread Every Level

The trickiest case. To update a nested field, spread every level on the path:

const [state, setState] = useState({
  user: { name: "Ada", address: { city: "London" } }
});

setState({
  ...state,
  user: {
    ...state.user,
    address: { ...state.user.address, city: "Bath" }
  }
});

The hassle of all that spreading is why people reach for:

  • Immer — write mutation-style code; it produces an immutable result under the hood
  • useReducer — described in chapter 5, helpful for complex shapes
  • Flatter state design — sometimes the cleanest fix is just to avoid deep nesting

With Immer

import { produce } from "immer";

setState(produce(draft => {
  draft.user.address.city = "Bath";   // looks like a mutation; isn't
}));

Immer hands you a “draft”, lets you mutate it, then produces a new immutable object. Popular with React + Redux.

Why React Cares

State updates trigger re-renders. React skips renders when the new state is Object.is-equal to the old. Mutating an object in place keeps the reference identical, so React sees nothing changed.

A new object with the same fields is a different reference → React re-renders → UI updates.

Up Next

A few more common state pitfalls collected in one place.

State Pitfalls →