setUser(u => ({ ...u, address: { ...u.address, city: 'Cairo' } }));
setTodos(t => [...t, newTodo]); // add
setTodos(t => t.filter(x => x.id !== id)); // remove
setTodos(t => t.map(x => x.id === id ? { ...x, done: true } : x)); // change
You must not mutate state, because React detects changes by comparing references. It compares the old and new value with Object.is. If you change an object and pass the same object back, React sees no change.
Mutation causes these problems:
- React can skip the re-render, so the screen does not update.
- Code that compares references sees no change. This includes
memo,useMemodependencies and effect dependencies. - It breaks the snapshot model, because older renders now see the changed data.
Instead, create a new object or array that contains the change. Spread the parts you keep and replace the parts you change. Copy each level of nesting you touch. For example, to change city, copy both the user and the address: { ...u, address: { ...u.address, city: 'Cairo' } }.
For arrays, use methods that return a new array:
- Add:
[...arr, item]. - Remove:
filter. - Change an item:
map.
Avoid push, splice, sort and reverse directly on state, because they change the original array. Copy first, or use toSorted. Immer is a popular helper library. It lets you write code that looks like mutation, and it produces new copies for you.
- React compares state with
Object.is. The same reference means no change. - Mutation breaks
memo, dependency arrays and the snapshots of older renders. - Copy each nested level you change, using spread.
- For arrays, use spread to add,
filterto remove andmapto update. - Immer can make deeply nested updates simpler.