Immutability in React state
Why is Immutability Important in React?
React relies on reference comparison to detect state changes. If you mutate an object directly, React won't detect the change and won't re-render. That's why state must always be updated immutably β creating a new object/array instead of modifying the existing one.
The Problem: Direct Mutation
tsx
const [user, setUser] = useState({ name: "Alice", age: 25 });
// β WRONG β mutates existing object
function updateAge() {
user.age = 26; // Same reference!
setUser(user); // React sees same reference β no re-render
}
// β WRONG β mutates existing array
const [items, setItems] = useState(["a", "b", "c"]);
function addItem() {
items.push("d"); // Mutates the original array
setItems(items); // Same reference β no re-render
}The Solution: Immutable Updates
Objects
tsx
const [user, setUser] = useState({ name: "Alice", age: 25 });
// β
Spread operator β creates new object
setUser({ ...user, age: 26 });
// β
Nested objects
const [state, setState] = useState({
user: { name: "Alice", address: { city: "Kyiv" } }
});
setState({
...state,
user: {
...state.user,
address: {
...state.user.address,
city: "Lviv"
}
}
});Arrays
tsx
const [items, setItems] = useState([1, 2, 3]);
// β
Add item
setItems([...items, 4]);
// β
Remove item
setItems(items.filter(item => item !== 2));
// β
Update item
setItems(items.map(item => item === 2 ? 20 : item));
// β
Insert at index
const index = 1;
setItems([...items.slice(0, index), 99, ...items.slice(index)]);Immutable Operations Cheat Sheet
| Operation | Mutating (β) | Immutable (β ) |
|---|---|---|
| Add to array | push(), unshift() | [...arr, item], [item, ...arr] |
| Remove from array | splice(), pop() | filter() |
| Replace in array | arr[i] = x | map() |
| Sort array | sort() | [...arr].sort() |
| Update object | obj.key = val | { ...obj, key: val } |
| Delete property | delete obj.key | const { key, ...rest } = obj |
Why React Needs Immutability
tsx
// React compares references (Object.is)
const prevState = { name: "Alice" };
const nextState = prevState;
prevState === nextState // true β React skips re-render
// New object = new reference
const nextState = { ...prevState, name: "Bob" };
prevState === nextState // false β React re-renders β
This is also why React.memo, useMemo, and useCallback work β they compare references.
Libraries for Complex Immutable Updates
Immer (most popular)
tsx
import { produce } from "immer";
const [state, setState] = useState({
users: [
{ id: 1, name: "Alice", scores: [90, 85] },
{ id: 2, name: "Bob", scores: [70, 75] },
]
});
// β
Immer lets you "mutate" a draft β produces immutable result
setState(produce(draft => {
const user = draft.users.find(u => u.id === 1);
if (user) {
user.name = "Alice Updated";
user.scores.push(95);
}
}));useImmerReducer
tsx
import { useImmerReducer } from "use-immer";
const [state, dispatch] = useImmerReducer(
(draft, action) => {
switch (action.type) {
case "addTodo":
draft.todos.push(action.payload); // "mutation" is safe with Immer
break;
case "toggleTodo":
const todo = draft.todos.find(t => t.id === action.id);
if (todo) todo.done = !todo.done;
break;
}
},
{ todos: [] }
);Important:
Never mutate React state directly. Always create new objects/arrays to trigger re-renders. Use the spread operator for simple updates and Immer for complex nested state. Immutability is fundamental to how React detects changes, optimizes rendering, and makes state predictable.
Short Answer
Interview readyPremium
A concise answer to help you respond confidently on this topic during an interview.