← Back to Chapters

State Immutability in React

⚛️ State Immutability in React

? Quick Overview

In React, state must be treated as immutable — you should never modify it directly. Instead, always create a new copy of the state, update that copy, and pass it to the setter function (like setState or setSomething).

  • React decides whether to re-render based on state references, not deep value checks.
  • Direct mutation keeps the same reference, so React often won’t re-render.
  • Use tools like the spread operator, map(), filter(), and concat() for safe updates.

? Key Concepts

  • Immutable state: Treat state as read-only; only update via a new object/array.
  • Reference changes: React re-renders when the state reference changes.
  • Objects & arrays: Always create new copies instead of editing existing ones.
  • Nested structures: Copy each level that you modify (shallow copying per level).
  • Performance: Immutability enables optimizations like React.memo and PureComponent.

? Syntax & Theory

Common immutable update patterns in React:

  • Updating objects: setUser(prev => ({ ...prev, age: prev.age + 1 }))
  • Adding to arrays: setTasks(prev => [...prev, "New task"])
  • Removing from arrays: setTasks(prev => prev.filter(t => t.id !== id))
  • Nested objects: setUser(prev => ({ ...prev, address: { ...prev.address, city: "Mumbai" } }))

These patterns always return a new reference, which lets React know that something changed and triggers a re-render.

? When You Mutate State Directly

Directly changing a state object (like user.age = 25) mutates the existing object instead of creating a new one. React does not detect this change reliably, so your UI may not update.

? View Code Example (Direct Mutation)
// React component that mutates state directly (not recommended)
function Example() {
const [user, setUser] = React.useState({ name: "Aman", age: 20 });

const handleClick = () => {
user.age = 25; // ❌ Direct mutation of the existing state object
console.log(user); // Logs updated object, but React may not re-render the UI
};

return (
<div>
<p>Age: {user.age}</p>
<button onClick={handleClick}>Update</button>
</div>
);
}

? Explanation

React compares the previous and next state values by reference. Here, the user object reference stays the same (only its inner value changes), so React assumes nothing changed and skips re-rendering. The console shows the new age, but the displayed UI may still show the old value.

✅ Correct Way: Creating a New Copy

The correct way is to use the setter function with a new state object. The spread operator ...prev copies existing properties into a fresh object, and then you override only what changed.

? View Code Example (Safe Object Update)
// React component that updates state immutably using a new object
function Example() {
const [user, setUser] = React.useState({ name: "Aman", age: 20 });

const handleClick = () => {
setUser(prev => ({ ...prev, age: prev.age + 1 })); // ✅ New object with updated age
};

return (
<div>
<p>Age: {user.age}</p>
<button onClick={handleClick}>Increase Age</button>
</div>
);
}

? Live Output (Conceptual)

Every time you click Increase Age, React receives a new user object. The reference changes, so React re-renders the component and shows the updated age on the screen.

? Immutability with Arrays

Arrays also must be updated by creating new arrays, not by mutating the existing one.

? View Code Example (Array Updates)
// Todo list that adds items using an immutable array update
function TodoList() {
const [tasks, setTasks] = React.useState(["Learn React", "Do homework"]);

const addTask = () => {
setTasks(prev => [...prev, "Practice coding"]); // ✅ New array using spread
};

return (
<div>
<ul>
{tasks.map((t, i) => <li key={i}>{t}</li>)}
</ul>
<button onClick={addTask}>Add Task</button>
</div>
);
}

? Explanation

Methods like push(), pop(), and splice() mutate the original array. Instead, use [...prev, item], concat(), filter(), or map() to return a brand-new array so React can detect the change.

? Nested Immutability (Deep Copying)

For nested objects, copy each level that you update. This is still a shallow copy at each level, but combined it behaves like a safe deep update for the fields you care about.

? View Code Example (Nested Object Update)
// Updating a nested address field without mutating original objects
function DeepExample() {
const [user, setUser] = React.useState({
name: "Riya",
address: { city: "Pune", pincode: 411001 },
});

const updateCity = () => {
setUser(prev => ({
...prev,
address: { ...prev.address, city: "Mumbai" }, // ✅ Copy address and update only city
}));
};

return (
<div>
<p>City: {user.address.city}</p>
<button onClick={updateCity}>Change City</button>
</div>
);
}

?️ Explanation

Only the city field changes, but we still create a new address object and a new user object. This preserves immutability and makes sure React re-renders with the new city.

? Why Immutability Matters

  • ✅ Ensures React can detect changes and re-render correctly.
  • ✅ Makes debugging easier and state updates more predictable.
  • ✅ Prevents accidental side effects in other parts of the app.
  • ✅ Enables optimization techniques like React.memo and PureComponent.

⚙️ Things to Avoid in State Updates

  • ❌ Using push(), pop(), or splice() directly on state arrays.
  • ❌ Modifying nested objects without copying inner layers (e.g., user.address.city = "X").
  • ❌ Expecting React to re-render after directly mutating state or props.

? Tips & Best Practices

  • Use the spread operator (...) or methods like map(), filter(), and concat() to update data.
  • When working with nested objects, copy only the parts that change at each level.
  • Never mutate props or previous state directly inside a setter like setState.
  • For complex state trees, consider using libraries like Immer to simplify immutable updates.

? Try It Yourself / Practice Tasks

  1. Create a counter and try updating it by mutation (count++) — observe that React may not re-render as expected.
  2. Fix the counter by using setCount(count + 1) or setCount(prev => prev + 1).
  3. Build a todo list where you add and remove items immutably using filter() and array spread / concat().
  4. Design a user profile object with nested fields and update only one nested key using the spread pattern.

Goal: Understand why React enforces immutability and learn how to update arrays and objects safely without breaking state reactivity.