Runnable lesson · press Try it on the examples to edit and run them here.
Mutation means changing a value in place: user.name = "Grace", list.push(4), settings.dark = true. In React state, you must never do this. Always create a new value and give it to the setter. This lesson explains why and gives you a checklist.
What React sees
When you call setUser(next), React compares next with the current state using Object.is. For objects and arrays that means the same reference. If you mutate an object and pass the same object back, React sees “same value” and skips the update.
function App() {
const [items, setItems] = React.useState(["a"]);
function addMutating() {
items.push("b"); // changes the existing array
setItems(items); // same reference: React bails out
}
return (
<div>
<p>Items: {items.length}</p>
<button onClick={addMutating}>Add (mutating)</button>
</div>
);
}
The array now holds two items, but the screen still says Items: 1. The data and the screen have separated. If anything else makes the component re-render later, the screen will suddenly show 2. That kind of delayed, confusing bug is the real danger of mutation.
The fix
function App() {
const [items, setItems] = React.useState(["a"]);
return (
<div>
<p>Items: {items.length}</p>
<button onClick={() => setItems([...items, "b"])}>Add</button>
</div>
);
}
[...items, "b"] is a new array, so React sees a change and re-renders.
Why immutability is worth it
- React can detect changes cheaply by comparing references.
- Memoization works.
React.memo,useMemoanduseEffectdependencies compare references. A mutated object is “the same”, so they do not update. - Undo and history become easy. Each old state is still intact.
- Bugs are local. Nothing else can change your data behind your back.
Where mutation hides
// Mutating (do not) // Copying (do)
user.age++ setUser({ ...user, age: user.age + 1 })
items.push(x) setItems([...items, x])
items.splice(i, 1) setItems(items.filter((_, n) => n !== i))
items[i] = y setItems(items.map((v, n) => (n === i ? y : v)))
items.sort() setItems([...items].sort())
const copy = items; copy.push(x) const copy = [...items]; copy.push(x) // copy first
obj.nested.value = 5 setObj({ ...obj, nested: { ...obj.nested, value: 5 } })
The line const copy = items copies the reference, not the data. copy.push still mutates the state array.
It is fine to mutate things you just created
Mutating a new array or object that you created in the same function is fine, because nobody else has seen it:
function handleAdd() {
const next = [...items]; // a copy
next.push(newItem); // mutating the copy is fine
next.sort();
setItems(next);
}
Ways to catch it
- Strict Mode. In development, React calls components and updater functions twice. Code that mutates state or props gives obviously wrong results, which makes the bug visible. See React Strict Mode.
- Freeze while developing.
Object.freeze(obj)makes mutations throw in strict-mode code. See Object.freeze. - TypeScript’s
readonlytypes stop mutation at compile time. See React with TypeScript. - Code review habit. Look for
push,pop,splice,sort,reverse, assignments to properties anddeletenear state.
Immer
Writing nested copies by hand can be tiring. Immer lets you write code that looks like mutation on a “draft”, and produces a safe immutable result for you. It is a fine tool when you have deeply nested state, though plain spreading is enough for most components.
Key points
- Mutating state in place hides the change from React because the reference stays the same.
- Always give the setter a new array or object, copying each level you change.
- Mutating a copy you made yourself is fine.
Exercise: Fix the cart
This button is meant to add “Tea” to the cart, but the screen does not change. Fix it. —solution
function App() {
const [cart, setCart] = React.useState([]);
function addTea() {
setCart([...cart, "Tea"]); // was: cart.push("Tea"); setCart(cart);
}
return (
<div>
<p>Cart: {cart.length} items</p>
<button onClick={addTea}>Add Tea</button>
</div>
);
}
The original called cart.push and then passed the same array to setCart, so React saw no change. Creating a new array fixes it.