Suggest an editImprove this articleRefine the answer for “Immutability in React state”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Immutability in React state** means creating new objects and arrays instead of modifying existing ones. React uses `Object.is()` to compare references, not values. Same reference means no re-render. ```tsx user.age = 26; setUser(user); // ❌ same reference, skipped setUser({ ...user, age: 26 }); // ✅ new reference, re-renders ``` **Key point:** React skips updates when the reference stays the same.Shown above the full answer for quick recall.Answer (EN)Image**Immutability in React state** means creating new objects or arrays instead of modifying existing ones, because React compares references to detect changes, not the actual values inside. ## Theory ### TL;DR - React uses `Object.is()` (reference equality) to check if state changed. Not deep comparison. - Mutating state directly keeps the same reference, so React sees no change and skips the re-render. - Create new objects with spread: `{ ...obj, key: newValue }`. Create new arrays with `map`, `filter`, or spread. - Decision rule: if you're writing `state.something = value`, you're mutating. That won't work. ### Quick example ```tsx const [user, setUser] = useState({ name: "Alice", age: 25 }); // ❌ Wrong - mutates in place, same reference, no re-render user.age = 26; setUser(user); // ✅ Right - new object, new reference, React re-renders setUser({ ...user, age: 26 }); // Component re-renders, displays age: 26 ``` The spread operator creates a new object in memory. React compares the old and new references, sees they differ, and queues a re-render. ### How React detects state changes React stores a reference to your state in its internal fiber tree. Every `setState` call triggers a comparison: `Object.is(prevState, nextState)`. If that returns `true`, React skips the component's render function entirely. If `false`, it schedules a re-render and reconciles the DOM. So `setUser(user)` after mutating `user.age` does nothing. The variable `user` still points at the same object in memory. React checks, gets `true`, moves on. This mechanism also explains why `React.memo`, `useMemo`, and `useCallback` work the way they do. All three are built on the same reference comparison. ### When to use each approach - Single field update on an object: `setUser({ ...user, age: 26 })` - Nested object update: spread each level, `setState({ ...state, user: { ...state.user, name: "Bob" } })` - Add to array: `setItems([...items, newItem])` - Remove from array: `setItems(items.filter(item => item.id !== id))` - Update one item in array: `setItems(items.map(item => item.id === id ? { ...item, ...changes } : item))` - Deeply nested state with many levels: use Immer ### Immutable operations reference | Operation | Mutating | Immutable | |---|---|---| | Add to array | `push()`, `unshift()` | `[...arr, item]` | | Remove from array | `splice()`, `pop()` | `filter()` | | Replace in array | `arr[i] = x` | `map()` | | Sort array | `sort()` | `[...arr].sort()` | | Update object field | `obj.key = val` | `{ ...obj, key: val }` | | Delete property | `delete obj.key` | `const { key, ...rest } = obj` | ### Common mistakes **1. Mutating a nested object** ```tsx // ❌ Wrong - outer reference unchanged, React skips re-render state.user.name = "Bob"; setState(state); // ✅ Right - new outer object AND new nested object setState({ ...state, user: { ...state.user, name: "Bob" } }); ``` React checks the outer object reference. It hasn't changed. Re-render skipped. **2. Using mutating array methods** ```tsx // ❌ Wrong - push() modifies the original, same reference items.push(newItem); setItems(items); // ✅ Right - new array reference setItems([...items, newItem]); ``` `push()`, `pop()`, `splice()`, `sort()`, and `reverse()` all modify the original array. Replace them with non-mutating alternatives. **3. Thinking spread covers nested objects** ```tsx // ❌ Partially wrong - drops any other fields address had setUser({ ...user, address: { city: "NYC" } }); // ✅ Right - spread the nested level too setUser({ ...user, address: { ...user.address, city: "NYC" } }); ``` Spread is shallow. It copies top-level properties, but nested objects are still shared references. **4. Mutating an item inside an array, then spreading the array** ```tsx // ❌ Wrong - new array, but todos[0] is still the same object reference const next = [...todos]; next[0].done = true; setTodos(next); // ✅ Right - new array, new item object setTodos(todos.map(todo => todo.id === id ? { ...todo, done: true } : todo )); ``` In practice, this is the sneakiest mutation bug. Developers who already know "don't mutate state directly" spread the array, feel confident, and then spend an hour debugging why the list item didn't update. The array is new. The objects inside are not. **5. Reading state right after setState** ```tsx // ❌ Wrong assumption - count is still the old value here setCount(count + 1); console.log(count); // ✅ Use the updater function when next state depends on previous setCount(prev => prev + 1); ``` `setState` schedules an update for the next render. It doesn't change the variable synchronously. ### Real-world usage - `useState` - every update needs a new reference to trigger re-render - Redux reducers - must return new state, never mutate the argument passed in - Zustand - store update functions should return new objects for predictable state - React Query - cache updates follow the same pattern for consistency - Immer - lets you write mutation-style code that produces immutable results under the hood - `React.memo` and `useMemo` - stable references prevent unnecessary re-renders of child components ### Follow-up questions **Q:** Why does React use reference comparison instead of deep equality? **A:** Deep equality checks every property recursively, which is O(n) per state update. Reference comparison is O(1). It also makes intent explicit: creating a new object signals "this is a changed value." **Q:** What if I accidentally mutate state but the component still re-renders because a parent prop changed? **A:** The component re-renders, but with corrupted state. Your mutation changed the old state object. React's internal state was never updated. The render uses whatever is in the fiber, ignoring your mutation. You get a visual mismatch and a hard-to-trace bug. **Q:** How does Immer solve deeply nested updates without endless spread operators? **A:** Immer wraps your state in a Proxy called a "draft." You write code that looks like direct mutation (`draft.user.address.city = "NYC"`), and Immer tracks every change. When you're done, it produces a new immutable object. It only copies branches that were actually modified, so it stays efficient even for large state trees. **Q:** I have 10,000 items in an array. Doesn't creating a new array on every update waste memory? **A:** Spreading large arrays is fast in modern JS engines, so it rarely matters. But if you update individual items frequently, normalize your state: store items as an object keyed by ID instead of an array. Then updating one item is a single property assignment, not a full array copy. Most apps never need to think about this. ## Examples ### Updating form state with nested preferences ```tsx const [formData, setFormData] = useState({ email: "", preferences: { notifications: true, theme: "dark" } }); function handleEmailChange(e) { // Only email changes; preferences reference stays the same - that's fine setFormData({ ...formData, email: e.target.value }); } function toggleNotifications() { // New outer object AND new preferences object setFormData({ ...formData, preferences: { ...formData.preferences, notifications: !formData.preferences.notifications } }); } ``` `handleEmailChange` creates a new top-level object. The `preferences` property still points at the same object, which is fine since it didn't change. `toggleNotifications` needs a new `preferences` object because that's the level that actually changed. ### Updating one item in an array of objects ```tsx const [todos, setTodos] = useState([ { id: 1, title: "Learn React", tags: ["js", "react"] }, { id: 2, title: "Build app", tags: ["project"] } ]); function addTagToTodo(todoId, newTag) { setTodos(todos.map(todo => todo.id === todoId ? { ...todo, tags: [...todo.tags, newTag] } : todo )); } ``` `map()` returns a new array. The matching todo gets a new object with a new `tags` array. Other todos keep their original references. That matters for `React.memo` on list items - unchanged items won't re-render. ### Complex nested updates with Immer ```tsx import { produce } from "immer"; const [state, setState] = useState({ users: [ { id: 1, name: "Alice", scores: [90, 85] } ] }); function addScore(userId, score) { setState(produce(draft => { const user = draft.users.find(u => u.id === userId); if (user) { user.scores.push(score); // Immer tracks this and produces a new object } })); } ``` Without Immer, this requires spreading the state object, spreading the `users` array, finding the user, spreading that user, and spreading the `scores` array. Five levels of spread for one change. Immer handles all of that internally. The intent is clear.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.