Why you should not mutate state directly.
React detects changes by reference (Object.is) — mutating state in place keeps the same reference, so React doesn't know it changed and skips the re-render. Mutation also breaks memo/PureComponent, corrupts previous-state snapshots, and makes time-travel/debugging unreliable. Always create a new object/array.
React's entire change-detection model is reference-based — and mutating state in place defeats it.
Why React can't see a mutation
When you call setState, React decides whether to re-render by comparing the new value to the old one by reference (Object.is). If you mutate the existing object/array:
const [user, setUser] = useState({ name: "Ada", age: 30 });
// ❌ MUTATION
user.age = 31;
setUser(user); // same reference! prevState === nextState → React bails out, NO re-renderYou changed the data, but the reference is identical, so React thinks nothing changed and skips the render. Your UI is now out of sync with your state. The fix is a new reference:
// ✅ new object
setUser({ ...user, age: 31 });
// ✅ new array
setItems([...items, newItem]);
setItems(items.filter(i => i.id !== id));The other things mutation breaks
1. React.memo / PureComponent / useMemo deps — all do reference comparison. Mutating a prop means the child sees prevProps === nextProps and won't update even though the data changed. Inconsistent rendering.
2. Previous-state snapshots are corrupted — features that rely on comparing previous vs next state (some hooks, dev tools, undo/redo, time-travel debugging) break, because the "previous" object is the "current" one — you mutated it.
3. Concurrent rendering — React may render the same component multiple times or pause/resume. Mutation makes renders non-deterministic and unsafe; React expects renders to be pure functions of immutable inputs.
4. Bugs that are hard to trace — a mutation in one place silently changes state another component holds a reference to.
The rule and the tools
Treat state as immutable. Always produce a new object/array when updating:
- Objects: spread
{ ...obj, changed }. - Arrays:
map,filter,concat, spread — notpush,splice,sort,pop(those mutate). - Nested updates: spread every level you change.
- For deep/complex updates, use Immer (RTK uses it) — you write "mutating" code, it produces an immutable update.
- Use the functional updater
setState(prev => ...)so you build the new value from the latest state.
The framing
"React detects state changes by reference equality. Mutating in place keeps the same reference, so setState sees prev === next and skips the re-render — the UI silently desyncs from the data. It also breaks every reference-based optimization — React.memo, useMemo — corrupts previous-state snapshots that dev tools and features rely on, and is unsafe under concurrent rendering, which assumes pure renders over immutable inputs. So state is immutable: always spread into a new object/array, use non-mutating array methods, and reach for Immer for deep updates."
Follow-up questions
- •How exactly does React decide whether to re-render after setState?
- •Which array methods mutate and which don't?
- •How does mutation break React.memo?
- •What is Immer and how does it let you write 'mutating' code safely?
Common mistakes
- •push/splice/sort/pop on an array in state, then setState with the same array.
- •Mutating a nested property and expecting a re-render.
- •Only spreading the top level when a nested value changed.
- •Mutating props passed to a child.
Performance considerations
- •Immutability enables React's cheap O(1) reference checks for re-render decisions and memoization. The cost is allocating new objects/arrays — almost always negligible, and far cheaper than the bugs and missed optimizations mutation causes.
Edge cases
- •Deeply nested state where you must spread every level.
- •sort() and reverse() mutating in place — copy first.
- •Mutating then calling setState with a new wrapper but stale inner references.
- •Shared references — two state slices pointing at the same object.
Real-world examples
- •A list that won't update because the code did items.push() then setItems(items).
- •Redux Toolkit using Immer so reducers can be written in a mutating style safely.