Updating objects and arrays in state
React compares old and new state with Object.is, so a mutated object looks unchanged. How to copy only the path you change, for objects and arrays.
After this lesson you can
- Explain why mutating state and then setting it may not re-render
- Update a nested object by copying only the changed path
- Pick the non-mutating array method for add, remove, replace and sort
When you call a setter, React compares the new state with the current one
using Object.is. If they are the same value, React treats the update as
a no-op and skips re-rendering. For a number that is what you want. For an
object or an array, Object.is compares references, not contents — so
an object you mutated in place and then passed back is the same object,
and React sees no change.
// Broken: same array, so Object.is(old, next) is true
todos.push(newTodo);
setTodos(todos);
// Works: a new array that shares the old items
setTodos([...todos, newTodo]);
The model below implements that comparison as a tiny store, counting how many renders each update causes.
function createStore(initial) { let state = initial; let renders = 0; return { get: () => state, set(next) { if (Object.is(state, next)) return; // React's bail-out check state = next; renders++; }, renders: () => renders, };} export function run() { const store = createStore(["milk"]); const list = store.get(); list.push("eggs"); // mutate in place store.set(list); // same reference const afterMutation = store.renders(); store.set([...store.get(), "bread"]); // new array const afterCopy = store.renders(); return { afterMutation, afterCopy, contents: store.get(), nanEqualsNaN: Object.is(NaN, NaN), zeroEqualsMinusZero: Object.is(0, -0), };}Object.is is === with two corrections: it treats NaN as equal to
itself and +0 as different from -0. The last two lines of the output
show both.
Copy the path, share the rest
Treat state as read-only and build a new value for every change. For nested data you copy each object on the path from the root to the thing that changed, and reuse everything else as it is:
function moveTo(user, city) { return { ...user, address: { ...user.address, city } };} export function run() { const user = { name: "Ada", address: { city: "London", street: "1 Main St" }, tags: ["admin"], }; const next = moveTo(user, "Paris"); return { oldCity: user.address.city, newCity: next.address.city, sameUser: next === user, sameAddress: next.address === user.address, sameTags: next.tags === user.tags, };}Sharing the untouched branches is not laziness — it is useful. A child
that receives user.tags gets the same reference as before, so a
memoised child (lesson 12) can skip its render.
Arrays without mutation
| To… | avoid (mutates) | use (returns a new array) |
|---|---|---|
| add | push, unshift | [...arr, x], [x, ...arr] |
| remove | splice, pop | filter |
| replace one | arr[i] = x | map, with(i, x) |
| sort / reverse | sort, reverse |
Try it yourself
2 visible tests · 5 hidden testsA component keeps const [items, setItems] = useState([]) and shows
the list. Each entry of ops is one event handler, run in order, and the
items a handler sees is always the current state array:
{ "op": "push", "item": s }—items.push(s); setItems(items);{ "op": "sortInPlace" }—items.sort(); setItems(items);{ "op": "append", "item": s }—setItems([...items, s]);{ "op": "remove", "item": s }—setItems(items.filter(x => x !== s));{ "op": "sorted" }—setItems(items.toSorted());
After a handler, React re-renders only if the value passed to the
setter is not Object.is-equal to the current state. Items are strings.
Implement listScreens(ops) returning { "shown": [...], "state": [...] }.
shown holds a copy of the list as it was at each render: the first
render, then every render a handler caused, in order. state is the
contents of the state array after the last handler.
listScreens([{"op":"push","item":"milk"},{"op":"append","item":"eggs"}])listScreens([{"op":"append","item":"b"},{"op":"append","item":"a"},{"op":"sortInPlace"},{"op":"sorted"}])
Sign up to run the tests, check the hidden ones and save your progress. Sign up