Skip to main content
Practice Problems

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

OperationMutating (❌)Immutable (βœ…)
Add to arraypush(), unshift()[...arr, item], [item, ...arr]
Remove from arraysplice(), pop()filter()
Replace in arrayarr[i] = xmap()
Sort arraysort()[...arr].sort()
Update objectobj.key = val{ ...obj, key: val }
Delete propertydelete obj.keyconst { 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

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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?
Practice Problems