In React you update an array or an object in state by making a new one and passing it to the setter. Never change the existing value in place: use spread (...), map, filter or slice to build a copy with your change, then call setItems(copy).
[...items, { ... }] is a new array with one more item, and filter returns a new array without the removed one. Both leave the old array alone.
Why mutation does not render
When you call a setter, React compares the new value with the old one using Object.is. For arrays and objects that compares references, not contents. If you change an array with push and pass the same array back, React sees the same reference, decides nothing changed, and skips the render.
Click "push" a few times. The console shows the array growing, but no new render line appears and the text stays a. Then click "spread": one render happens and every pushed item shows up at once, because they were in the array all along. Mutation does not throw an error, it just leaves the screen out of date until something else triggers a render. Click "push" once more after that and this time it may render: React cannot always skip the work up front, so a mutation shows up sometimes and not other times. That unpredictability is the reason not to mutate state.
Arrays: methods to use and to avoid
The rule is simple: use methods that return a new array, avoid methods that change the array they are called on.
| Goal | Avoid (mutates) | Use (returns a new array) |
|---|---|---|
| Add | push, unshift | [...arr, item], [item, ...arr] |
| Remove | pop, shift, splice | filter, slice |
| Replace | arr[i] = x, splice | map, with(i, x) |
| Insert | splice | [...arr.slice(0, i), x, ...arr.slice(i)], toSpliced |
| Sort, reverse | sort, reverse | toSorted, toReversed, or copy first |
toSorted, toReversed, toSpliced and with are newer array methods (ES2023), supported in all current major browsers since 2023. They do the same job as their mutating cousins on a copy.
Updating one item with map
To change one item, map over the array and return a new object for the item you want, and the original object for every other one.
The { ...todo, done: !todo.done } part matters. A common bug is to copy the array but change the object inside it:
const next = [...todos];
next[0].done = true; // next is new, but next[0] is the same object as todos[0]
setTodos(next);
This renders, so it looks fine, but the old array and the new array now share a changed object. Anything that compares the old item with the new one (a React.memo child, an effect that depends on the item) sees no difference. Copy the item you change.
Inserting at an index
There is no non-mutating insert, so slice the array in two and put the new item between the halves. toSpliced(index, 0, item) does the same in one call.
Set the index to 0 and insert again: the new C goes to the front. Replace the next expression with letters.toSpliced(index, 0, 'C') and the result is the same.
Sorting and reversing
sort and reverse change the array they are called on, even though they also return it. That makes setItems(items.sort()) a mutation followed by a set with the same reference, which is the bug from the start of this page. Sort a copy instead.
[...scores].reverse() works because the spread makes the copy first and reverse then changes only that copy.
Updating objects and nested objects
Objects follow the same rule. Spread the old object into a new one and override the fields that change. For a nested object, spread at every level on the way down to the field: a single spread copies only the top level, so the inner object would still be the old one.
Each level you copy is a new object; every level you do not touch stays the same object. That is what makes spread cheap: if the state also held a long list of orders, { ...user } would copy the reference to the list, not the list itself.
If an object in state is nested three or four levels deep and you keep writing long chains of spreads, that is often a sign the state could be flatter. Storing items in a lookup by id, or splitting one big object into a few useState calls, keeps each update short. When many events change one structure, a reducer also helps (see useReducer).
structuredClone vs spread
structuredClone(user) makes a deep copy in one call, so you can then change the copy freely:
const next = structuredClone(user);
next.address.city = 'Oslo';
setUser(next);
It is correct, but it copies the whole object every time, including the parts you did not change. That costs time on large state, and it means every nested object gets a new reference, so a memoized child that receives user.orders renders again even though the orders did not change. structuredClone also throws on functions, and class instances come back as plain objects. Spread copies only the path you change, so it is the usual choice; keep structuredClone for the rare update that really rewrites most of the object.
Writing mutations with Immer
Immer is a small library that lets you write code that looks like mutation. It hands you a draft, records what you change, and produces a new immutable value with only the changed parts copied. The use-immer package wraps it in a hook:
import { useImmer } from 'use-immer';
function Profile() {
const [user, updateUser] = useImmer({
name: 'Ada',
address: { city: 'London' },
});
function moveTo(city) {
updateUser((draft) => {
draft.address.city = city;
});
}
// ...
}
Immer is not available in the editors on this page, which only load React. It is worth adding when your state is deeply nested and the spreads get hard to read. For flat state, plain spread and map are enough, and they are what you will see in most React code.
Frequently Asked Questions
How do you add an item to an array in React state?
Create a new array that contains the old items plus the new one and pass it to the setter: setItems([...items, newItem]). Do not call items.push(newItem), because that changes the existing array and React sees the same reference.
Why does my component not re-render when I update an array?
You most likely changed the array in place (push, splice, sort, or arr[i] = x) and passed the same array back. React compares the old and new value with Object.is, finds the same object, and skips the render. Pass a new array instead.
How do you update one object inside an array in React?
Use map and return a new object for the item that changes, and the original item for every other one: setTodos(todos.map(t => t.id === id ? { ...t, done: !t.done } : t)).
How do you update a nested object in React state?
Copy every level on the path to the field you change: setUser({ ...user, address: { ...user.address, city: 'Oslo' } }). A single spread copies only the top level, so the inner object would still be shared.
Should I use structuredClone or the spread operator for React state?
Prefer spread. It copies only what changes and keeps the untouched parts as the same objects, which helps React.memo and comparisons. structuredClone copies everything every time, which is simpler to write but slower on large state and breaks reference equality for parts that did not change.