Detailed explanation
You should not mutate React state directly because state represents a snapshot for a particular render, not a shared container to edit whenever you want. React expects updates to be requested through a state setter, with a new value when something changes. Immutability helps React detect updates reliably and keeps earlier render snapshots intact. This matters especially when state is passed to children, captured by event handlers, or used in effects: changing an existing object can silently alter data that other code still references.
For useState, React compares the proposed state with the current state using Object.is. Objects and arrays are compared by identity, not by inspecting their contents. For example, items.push(x); setItems(items); changes the existing array but passes back the same reference. React can skip rendering the update because the state appears unchanged by this comparison. Calling push without a setter does not schedule a render at all. Importantly, mutation does not guarantee that the screen will never change: another update might trigger a render and expose the mutation later, making the behaviour confusing.
Instead, create a replacement value and pass it to the setter. To append an item based on the previous array, use setItems(previous => [...previous, x]). To update a property, use setUser(previous => ({ ...previous, name: 'New' })). The functional updater receives the pending state, so it works correctly when React queues multiple updates. Spread syntax creates only a shallow copy; for nested updates, copy every object or array along the changed path. Unchanged branches can retain their references, which is called structural sharing and avoids unnecessary copying.
In an interview, connect immutability to both correctness and optimisation. React.memo usually compares individual props with Object.is, and effect dependencies also use identity-based comparisons, so mutated references can hide meaningful changes from these mechanisms. Immutable updates also make debugging easier because previous snapshots remain trustworthy. You do not need to deep-clone the entire state or avoid all mutation everywhere: modifying a newly created, unshared object before storing it is fine. The rule is to avoid changing objects already held in React state.
Key points to remember
- Treat state objects and arrays as immutable render snapshots.
- useState uses Object.is; changing contents does not change an object's identity.
- Use setters with replacement values to request updates reliably.
- Use functional updaters when the next value depends on previous state.
- Copy each level along a nested update path while preserving unchanged references.
Common mistakes candidates make
- Using push, splice, or sort on the state array and then passing the same array to its setter.
- Copying only the outer object and then mutating a nested object shared with previous state.
- Assuming mutation always prevents rendering; unrelated renders can reveal mutated data later.
Likely follow-up questions
Why prefer setItems(previous => [...previous, x]) over setItems([...items, x])?
Both create a new array, but the functional version derives its result from the pending state in React's update queue. It avoids losing updates when multiple queued updates would otherwise use the same captured items value.
How do you update a nested state property?
Copy every level along the changed path, for example: setUser(previous => ({ ...previous, address: { ...previous.address, city: 'Pune' } })). Other unchanged branches can keep their existing references.
Does Immer allow you to mutate React state safely?
Immer lets you write mutation-like operations against a draft rather than the original state. It tracks those edits and produces an immutable result, preserving references for unchanged branches.
Interview tip: Explain reference identity first, demonstrate a functional immutable update, and finish with the shallow-copy caveat.
