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
// 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 objectThe 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:
useStateexpects 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
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 untouchedMistake 2: Shallow copy doesn't protect nested objects
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" - protectedShallow 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
// 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
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;
useStateexpects 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(), andwith()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
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
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 safestructuredClone() is available in modern browsers and Node 17+. For older environments, Immer or Lodash's cloneDeep cover the same ground.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.