// โ Wrong - mutating state directly
const [user, setUser] = useState({ name: "Ravi", age: 20 });
user.age = 21; // React has no way to know this happened
setUser(user); // same object reference - React may skip the re-render entirelyReact compares object references to decide whether to re-render, not deep contents. Mutating the existing object and passing the same reference back to setUser() means React might not detect any change at all.
const [user, setUser] = useState({ name: "Ravi", age: 20 });
function haveBirthday() {
setUser((previousUser) => ({
...previousUser,
age: previousUser.age + 1
}));
}...previousUser copies every existing property into a brand-new object, and age: previousUser.age + 1 overwrites just that one field โ React sees a genuinely new object reference and re-renders correctly.
const [todos, setTodos] = useState(["Learn JSX", "Learn Props"]);
// Add - spread the old array into a new one
function addTodo(text) {
setTodos((prev) => [...prev, text]);
}
// Remove - filter creates a new array without the removed item
function removeTodo(index) {
setTodos((prev) => prev.filter((_, i) => i !== index));
}
// Update - map creates a new array with one item changed
function updateTodo(index, newText) {
setTodos((prev) => prev.map((todo, i) => (i === index ? newText : todo)));
}Notice the pattern: filter() and map() both naturally return new arrays without touching the original โ this is exactly why they're the standard tools for updating array state, instead of methods like push() or splice() which mutate in place.
todos.push(newItem) mutates the existing array in place, exactly like the object mutation mistake above โ React may not detect the change and won't re-render. Always create a new array using spread, map(), or filter(), and pass that new array to your state setter.
Build a simple todo list where you can add a new item and remove an item by index, using proper immutable array updates.
import { useState } from "react";
function TodoApp() {
const [todos, setTodos] = useState(["Learn React"]);
function addTodo() {
setTodos(prev => [...prev, "New Task"]);
}
function removeTodo(index) {
setTodos(prev => prev.filter((_, i) => i !== index));
}
return (
<div>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map((todo, i) => (
<li key={i}>
{todo} <button onClick={() => removeTodo(i)}>Remove</button>
</li>
))}
</ul>
</div>
);
}
Q What does 'immutability' actually mean in React?
It means never changing an existing object or array in place โ instead, always create a brand-new copy (using spread, map, or filter) with the desired changes, and pass that new copy to your state setter.
Q Why does React care about object references instead of just comparing values?
Deeply comparing every value inside every object on every render would be slow at scale. Comparing references (is this the exact same object in memory?) is extremely fast, which is why React relies on you creating new objects/arrays to signal that something actually changed.