React

Why does React recommend against mutating state?

The short answer

React uses reference comparison (Object.is) to decide if state has changed. If you mutate an object or array in place, its reference stays the same, so React thinks nothing changed and skips the re-render. Your data updates, but the screen doesn't. Creating new references — through spreading, mapping, or filtering — tells React "this is different now, please re-render."

What mutation actually means

Mutation means changing a value in place. You're modifying the existing object or array rather than creating a new one.

// Mutation — changing the original array
const items = ['apple', 'banana'];
items.push('cherry');

// No mutation — creating a new array
const items = ['apple', 'banana'];
const newItems = [...items, 'cherry'];

After the mutation, items is still the same array (same reference in memory), it just has a new element. After the spread, newItems is a completely different array — a new reference.

This distinction is the foundation of everything that follows.

Why React needs new references

When you call a state setter like setItems, React compares the new value to the old value using Object.is. For objects and arrays, Object.is checks reference equality — it asks "is this the exact same object in memory?" not "do the contents look the same?"

Here's the bug that mutation creates:

function ShoppingList() {
const [items, setItems] = useState(['apple', 'banana']);

const addItem = () => {
items.push('cherry');
setItems(items);
};

return (
<div>
{items.map((item, i) => (
<p key={i}>{item}</p>
))}
<button onClick={addItem}>Add cherry</button>
</div>
);
}

You click the button. items.push('cherry') adds the item to the array. Then setItems(items) passes the same array reference to React. React compares old and new with Object.is — same reference, so it concludes nothing changed. No re-render. The screen stays the same even though the underlying data has been modified.

The fix is to create a new array:

const addItem = () => {
setItems([...items, 'cherry']);
};

Now setItems receives a brand new array. Object.is sees a different reference, so React re-renders, and the new item appears on screen.

The same problem with objects

The exact same issue happens with objects. Direct property assignment mutates the object in place:

function Profile() {
const [user, setUser] = useState({
name: 'Sarah',
age: 28,
});

const handleBirthday = () => {
// Bug: mutation — same reference, no re-render
user.age = 29;
setUser(user);
};

return (
<div>
<p>
{user.name} is {user.age}
</p>
<button onClick={handleBirthday}>Birthday</button>
</div>
);
}

The fix is to spread into a new object:

const handleBirthday = () => {
setUser({ ...user, age: 29 });
};

The spread creates a new object with the same properties, except age is overwritten with the new value. New reference, React re-renders, the UI updates.

The immutable update patterns

Once you understand the principle, the patterns become second nature.

Adding to an array:

setItems([...items, newItem]);

Removing from an array:

setItems(items.filter((item) => item.id !== idToRemove));

Updating an item in an array:

setItems(
items.map((item) =>
item.id === targetId
? { ...item, completed: true }
: item
)
);

Updating a nested object:

setUser({
...user,
address: {
...user.address,
city: 'New York',
},
});

Each of these produces a new reference at every level of nesting that changed. React can see the difference and re-renders accordingly.

Why this also matters for performance optimizations

Immutability isn't just about making re-renders happen. It's also about preventing unnecessary re-renders — which matters as your app grows.

React.memo wraps a component and skips re-rendering if its props haven't changed. It uses the same Object.is comparison:

const TodoItem = memo(function TodoItem({
todo,
onToggle,
}) {
return (
<li onClick={() => onToggle(todo.id)}>
{todo.completed ? '✓' : '○'} {todo.text}
</li>
);
});

If you're mutating the todo objects instead of creating new ones, every todo in the list has the same reference as before, so memo thinks nothing changed and skips rendering all of them — even the one that was actually toggled. The optimization backfires.

With immutable updates, only the toggled todo gets a new reference. memo correctly re-renders that one item and skips the rest. This is how you get efficient list rendering in React.

The same logic applies to useMemo and useCallback. Their dependency arrays use Object.is to decide whether to recompute. Stable, immutable references make these hooks work correctly.

Nested updates get verbose — that's where Immer helps

The one downside of immutable updates is that deeply nested state gets painful:

setCompany({
...company,
departments: company.departments.map((dept) =>
dept.id === deptId
? {
...dept,
employees: dept.employees.map((emp) =>
emp.id === empId
? { ...emp, role: 'Senior' }
: emp
),
}
: dept
),
});

That's hard to read and easy to get wrong. Immer is a library that lets you write code that looks like mutation, but actually produces immutable updates under the hood:

import { produce } from 'immer';

setCompany(
produce((draft) => {
const dept = draft.departments.find(
(d) => d.id === deptId
);
const emp = dept.employees.find((e) => e.id === empId);
emp.role = 'Senior';
})
);

Immer creates a draft copy, lets you "mutate" it freely, then produces a new immutable state with only the changed parts replaced. You get the readability of mutation with the safety of immutability.

The mental model

Think of React state like a series of snapshots. Each render is a photograph of your state at that moment in time. When you mutate, you're editing a photograph after it was taken — React already looked at it and moved on. When you create a new object, you're giving React a new photograph to compare with the previous one. That comparison is how React knows what changed and what to update on screen.

Why interviewers ask this

This question gets to the heart of how React's rendering model works. It tests whether you understand reference equality, why Object.is matters for state comparisons, and how immutability connects to both correctness (re-renders actually happen) and performance (React.memo, useMemo). Knowing the immutable update patterns and being able to explain why mutation fails shows you have a deep understanding of React's design principles, not just its API.