Skip to main content

What is immutability?

Immutability - once you create a value, you don't modify it in place. You create a new value with your desired changes, leaving the original untouched.

Theory

TL;DR

  • Like a photo: you can't edit the original, but you can take a new shot with different settings
  • Mutating changes an existing object at the same memory address; immutability allocates a new one
  • React and Redux compare object references to detect changes, not their contents, so immutability is not optional there
  • Use it when multiple parts of your code share the same object, or when you need predictable state transitions

Quick example

javascript
// Mutation - changes the original const user = { name: "Alice", age: 25 }; user.age = 26; console.log(user); // { name: "Alice", age: 26 } - original modified // Immutability - creates a new object const user2 = { name: "Alice", age: 25 }; const updatedUser = { ...user2, age: 26 }; console.log(user2); // { name: "Alice", age: 25 } - unchanged console.log(updatedUser); // { name: "Alice", age: 26 } - new object

The spread operator creates a new object in memory. The original stays at its old memory address, untouched.

Key difference

When you mutate, you're modifying data at an existing memory address. Any other variable pointing to that address sees the change too. With immutability, you leave the original alone and allocate new memory for the updated version. Code that holds a reference to the old object keeps seeing the old data, which is exactly what you want when tracking state transitions.

When to use

  • React state: useState expects a new object reference to trigger a re-render; mutating and passing the same reference does nothing
  • Redux reducers: always return a new state object, never modify the previous one
  • Shared data: when two or more functions reference the same object, mutations create bugs that are hard to trace
  • Function parameters: returning a new array instead of mutating the input keeps side effects out of the caller's scope
  • Async code: when multiple operations access the same data, immutable values prevent race conditions

Common mistakes

Mistake 1: Assignment is not a copy

javascript
const original = { count: 0 }; const copy = original; // same reference, not a copy copy.count = 1; console.log(original.count); // 1 - both point to the same object // Right const safeCopy = { ...original }; safeCopy.count = 1; console.log(original.count); // 0 - original untouched

Mistake 2: Shallow copy doesn't protect nested objects

javascript
const user = { name: "Alice", settings: { theme: "dark" } }; const copy = { ...user }; copy.settings.theme = "light"; console.log(user.settings.theme); // "light" - nested object was mutated // Right: spread both levels const safeCopy = { ...user, settings: { ...user.settings, theme: "light" } }; console.log(user.settings.theme); // "dark" - protected

Shallow copy duplicates only the top-level properties; nested objects still share the same reference. I've seen this catch developers who were sure they were writing immutable code.

Mistake 3: Some array methods mutate

javascript
// These mutate the original nums.push(4); nums.splice(0, 1); nums.sort(); // These return new arrays const added = [...nums, 4]; const removed = nums.slice(1); const sorted = [...nums].sort();

Mistake 4: Mutating state in React

javascript
const [items, setItems] = useState([1, 2, 3]); // Wrong - React sees the same reference, skips re-render items.push(4); setItems(items); // Right - new reference triggers re-render setItems([...items, 4]);

Real-world usage

  • React: props and state are treated as immutable; useState expects new references to detect changes
  • Redux: reducers return new state objects on every action
  • Immer.js: write mutation-style code that produces immutable updates internally
  • JavaScript itself: toSorted(), toReversed(), toSpliced(), and with() are newer array methods that return new arrays instead of mutating

Follow-up questions

Q: Is const the same as immutability?
A: No. const prevents reassignment of a variable, but the object can still be mutated. const obj = {}; obj.prop = "value" is valid. Immutability is about the object's contents, not the variable binding.

Q: What is the performance cost of creating new objects everywhere?
A: Modern JavaScript engines handle it well. For large or deeply nested data, libraries like Immer use structural sharing - only the changed path is copied, everything else reuses the same references. Memory usage stays manageable.

Q: How do you handle deeply nested updates without verbose spreading?
A: Immer.js. You write produce(state, draft => { draft.user.address.city = "LA"; }) and get a new immutable state without chaining spread operators at every level.

Q: (Senior) What is structural sharing and how does Immer use it?
A: Structural sharing means unchanged branches of a data tree are reused between versions. When you update one property, only the objects along that path get copied; everything else is the same reference as before. This is how Immer and Immutable.js keep updates efficient at O(log n) instead of O(n).

Examples

Intermediate: Immutable state updates in React

javascript
function UserProfile() { const [user, setUser] = useState({ name: "Alice", address: { city: "NYC", zip: "10001" } }); const updateCity = (newCity) => { setUser({ ...user, address: { ...user.address, city: newCity // only this field changes } }); }; return <button onClick={() => updateCity("LA")}>Move to LA</button>; }

Updating a nested field requires spreading both the outer object and the nested one. Skip either and you produce a mutation React won't detect.

Advanced: The array reference trap

javascript
const items = [1, 2, 3]; const newItems = items; // reference, not a copy newItems.push(4); console.log(items); // [1, 2, 3, 4] - both changed console.log(items === newItems); // true - same object in memory // Shallow copy is better, but not perfect for nested structures const copy = items.slice(); copy.push(4); console.log(items); // [1, 2, 3] - safe at the top level // Nested arrays still share references const matrix = [[1, 2], [3, 4]]; const shallowCopy = matrix.slice(); shallowCopy[0][0] = 99; console.log(matrix); // [[99, 2], [3, 4]] - inner array was mutated // structuredClone solves this const deepCopy = structuredClone(matrix); deepCopy[0][0] = 99; console.log(matrix); // [[1, 2], [3, 4]] - original safe

structuredClone() is available in modern browsers and Node 17+. For older environments, Immer or Lodash's cloneDeep cover the same ground.

Short Answer

Interview ready
Premium

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

Finished reading?