Suggest an editImprove this articleRefine the answer for “Immutability and mutability in JavaScript”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Mutability** means changing an object's contents in place. **Immutability** means creating a new object for every change, keeping the original intact. ```js const user = { name: 'Alice' }; // Mutable: changes in place, all refs affected user.name = 'Bob'; // Immutable: new object, original untouched const updated = { ...user, name: 'Charlie' }; console.log(user.name); // 'Bob' console.log(updated.name); // 'Charlie' ``` **Key point:** React compares state by reference (`===`). Mutating the same object won't trigger a re-render. Always return a new reference when updating shared state.Shown above the full answer for quick recall.Answer (EN)Image**Mutability** means you can change an object's contents directly after creation. **Immutability** means you create a new object for any change, leaving the original untouched. ## Theory ### TL;DR - Mutable objects are like a shared whiteboard: whoever holds a reference changes the same data in place - Immutable updates work like photocopies: edits produce a fresh copy, the original stays as-is - Main difference: mutation alters a shared memory location; immutability allocates new memory and decouples references - Use immutability for shared state (React props, Redux store); use mutability for private, performance-critical loops - `const` does NOT make objects immutable. It only blocks reassignment. ### Quick example ```js // Mutable: all references see the change const mutableUser = { name: 'Alice' }; const ref = mutableUser; mutableUser.name = 'Bob'; console.log(ref.name); // 'Bob' - shared reference changed // Immutable: original stays intact const immutableUser = { name: 'Alice' }; const updated = { ...immutableUser, name: 'Bob' }; console.log(immutableUser.name); // 'Alice' - unchanged console.log(updated.name); // 'Bob' ``` Spread (`...`) copies own properties into a new object at a new memory address. The two variables now point to different locations, so changes to one do not affect the other. ### Key difference JavaScript objects are reference types. A mutable operation like `obj.name = 'Bob'` rewrites a property in the heap slot that every variable pointing to `obj` shares. An immutable operation like `{ ...obj, name: 'Bob' }` allocates fresh heap space and shallow-copies own properties into it. O(n) time cost, but the original reference is completely isolated. ### When to use - Shared UI state (React props, useState) - immutability. React compares by reference (`===`), not by content. Mutation on the same reference won't trigger re-render. - Redux store - immutability. Enables time-travel debugging and predictable state history. - Performance-critical private data (tight loops, large buffers) - mutability. No copy overhead. - Pure functions and tested utilities - immutability. Predictable inputs and outputs, memoization works correctly. - Single-owner local variables - mutability. Simpler code, no cost. ### Comparison table | Property | Mutability | Immutability | |---|---|---| | Data change | In-place on original | New object created | | React re-render | May not trigger (same ref) | Always triggers (new ref) | | Side effects | Possible across references | Isolated by design | | Debugging | Harder to trace | Clear change history | | Performance | No copy overhead | O(n) copy cost | | Typical use | Private local data, buffers | Shared state, Redux, props | ### How V8 handles this V8 stores objects as heap-allocated structures. `obj.prop = val` rewrites the property slot in place via pointer dereference - no allocation, constant time. `{ ...obj }` triggers `OrdinaryObjectCreate` + `CopyDataProperties`: new heap space, shallow copy of own enumerable properties. The copy costs O(n) relative to property count. Shallow means nested objects still share references, which is the source of the most common gotcha with spread. ### Common mistakes **Assuming spread does a deep copy:** ```js const state = { user: { profile: { friends: ['Alice'] } } }; const copy = { ...state }; // shallow copy only! copy.user.profile.friends.push('Bob'); console.log(state.user.profile.friends); // ['Alice', 'Bob'] - original mutated ``` Spread copies only top-level properties. Nested objects still share the same reference. The nested mutation trap is the one I've seen catch developers most often when they first move to a React codebase. Fix: use `structuredClone(state)` (Node 17+, Chrome 98+) for a true deep copy. **Mutating state directly in React:** ```js // Wrong: same reference, React skips re-render const markDoneWrong = () => { todos[0].done = true; setTodos(todos); // shallow ref check fails - UI freezes }; // Correct: new array with new object const markDoneCorrect = () => { setTodos(todos.map(todo => todo.id === 1 ? { ...todo, done: true } : todo )); }; ``` React's reconciler uses shallow equality on references. If the reference didn't change, React skips the re-render entirely. No error, no warning - just stale UI. **Relying on `const` for immutability:** ```js const arr = [1, 2]; arr.push(3); // works fine, no error console.log(arr); // [1, 2, 3] ``` `const` blocks reassignment of the binding, not mutation of the value. Use `Object.freeze(arr)` for shallow immutability, or full immutable patterns for anything nested. **Mutating function arguments:** ```js // Wrong: impure, breaks React.memo and Redux selectors function addItem(list, item) { list.push(item); return list; } // Correct: returns new array function addItem(list, item) { return [...list, item]; } ``` Memoization relies on stable input references. Mutate the input and the memo cache becomes unreliable. **Expecting `Object.assign` to deep-clone:** ```js const copy = Object.assign({}, state); // shallow copy.nested.arr.push(1); // mutates state.nested.arr ``` `Object.assign` copies own enumerable properties at the top level only. Same trap as spread. Use `structuredClone` or `_.cloneDeep` from lodash when you need full depth. ### Real-world usage - React useState: `setState({ ...prev, name: 'Bob' })` - official docs pattern, creates new reference for reconciler - Redux Toolkit: `createSlice` uses Immer internally, proxies mutations on a draft and produces an immutable patch - Immer: lets you write `state.user.name = 'Bob'` inside `produce()`, Immer converts it to an immutable update automatically - Lodash: `_.cloneDeep(obj)` in Express middleware for safe cloning of request bodies - Node.js streams: internal Buffers are mutable for performance, but event payloads passed between handlers are copied immutably ### Follow-up questions **Q:** What does this log and why: `const a = [1]; const b = a; a.push(2); console.log(b);` **A:** `[1, 2]`. Arrays are reference types. `b` and `a` point to the same heap location. `push` mutates in place, so `b` reflects the change. **Q:** Why doesn't React re-render when you mutate state directly? **A:** React's reconciler compares references with `===`. If `setState` receives the same reference, the check reads as "no change" and the render cycle is skipped. **Q:** What is the performance cost of immutability? **A:** A shallow copy like spread costs O(n) where n is the number of own properties. Libraries like Immer use structural sharing: only the changed path is copied, unchanged branches keep their original reference. This cuts allocation cost significantly for large nested state. **Q:** How is `Object.freeze` different from true immutability? **A:** `Object.freeze` makes one object level read-only without allocating a new object. It throws in strict mode, silently fails otherwise. Nested objects remain mutable. True immutability in Redux patterns means a new reference on every update. **Q:** When would you choose `structuredClone` over `JSON.parse(JSON.stringify(x))`? **A:** Almost always. `structuredClone` handles circular references, `Date`, `Map`, `Set`, and typed arrays correctly. `JSON.parse/stringify` drops `undefined` and functions, turns `Date` into strings, and breaks on circular refs. The only reason to use `JSON.parse/stringify` is supporting environments before Chrome 98 or Node 17 without a polyfill. **Q (senior):** How does Immer's draft proxy enable efficient immutable updates? **A:** Immer wraps state in a Proxy. Inside `produce()`, it tracks which paths were accessed and modified. On commit, it applies structural sharing: untouched subtrees keep original references, only modified nodes are copied. A single field update on a large object costs O(depth), not O(total properties). That is why Redux Toolkit defaults to Immer instead of manual spread at every nesting level. ## Examples ### Shared reference gotcha ```js const original = { name: 'Alice', scores: [10, 20] }; const copy = { ...original }; copy.name = 'Bob'; copy.scores.push(30); // mutates original.scores! console.log(original.name); // 'Alice' - top-level fine console.log(original.scores); // [10, 20, 30] - nested array changed ``` Spread creates a new top-level object but nested properties still share the same reference. To safely update a nested array: `{ ...original, scores: [...original.scores, 30] }`. ### React todo update ```js const [todos, setTodos] = useState([ { id: 1, text: 'Learn JS', done: false } ]); // Wrong: mutates original, React skips re-render const markDoneWrong = () => { todos[0].done = true; setTodos(todos); // same reference - UI stays stale }; // Correct: new array with new object for changed item const markDoneCorrect = () => { setTodos(todos.map(todo => todo.id === 1 ? { ...todo, done: true } : todo )); }; ``` `map` returns a new array. The spread inside creates a new todo object. React gets a new reference, detects a change, and re-renders. ### Deep clone with structuredClone ```js const state = { user: { profile: { friends: ['Alice'] } } }; // Node 17+, Chrome 98+ const safeCopy = structuredClone(state); safeCopy.user.profile.friends.push('Bob'); console.log(state.user.profile.friends); // ['Alice'] - original safe console.log(safeCopy.user.profile.friends); // ['Alice', 'Bob'] ``` `structuredClone` does a true deep copy. It handles nested objects, arrays, `Date`, `Map`, `Set`, and circular references. It does not copy functions or symbols, so keep that in mind for objects with methods.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.