Immutability
The Museum Exhibit Behind Glass
LinkedIn Hook
Object.freeze(config)does NOT make your config immutable.If you wrote
config.headers.Authorization = "Bearer stolen-token"right after freezing, the write would succeed. BecauseObject.freezeis shallow — it only protects the top level. Nested objects are still fully mutable. Your frozen "constants" are silently mutable the moment anything has depth.This is the bug that ships in React apps every week — a "safe" state object gets mutated one level deep, React doesn't re-render because the reference didn't change, and the UI drifts out of sync with reality.
In this lesson you'll learn why immutability matters for React's reference-equality change detection, the difference between
const,Object.seal, andObject.freeze, how to write a properdeepFreeze, the spread-based immutable update pattern, and how Immer turns "mutative" code into immutable results via structural sharing.If "const makes it immutable" has ever slipped out of your mouth in an interview — this lesson fixes it.
Read the full lesson -> [link]
#JavaScript #InterviewPrep #Immutability #React #Frontend #CodingInterview #WebDevelopment
What You'll Learn
- Why immutability is required for React's reference-equality change detection
- The real differences between
const,Object.seal, andObject.freeze - Immutable update patterns: spread, deep freeze, Immer, and structural sharing
The Museum Exhibit Behind Glass
Think of immutability like a museum exhibit behind glass. You can look at it (read), but you can't touch it (mutate). If you want a version with changes, you create a new exhibit (new object) inspired by the original — but the original stays untouched forever.
Why Immutability Matters
- Predictable State: If data never changes, you always know what it contains. No mysterious mutations from distant code.
- Debugging: When state is immutable, you can track every change as a new snapshot — perfect for time-travel debugging (Redux DevTools).
- Performance in React: React uses reference comparison (
===) to detect changes. If you mutate an object, its reference doesn't change, so React skips re-rendering. Immutable updates create new references, triggering renders correctly.
// BAD — mutation breaks React's change detection
const [user, setUser] = useState({ name: "Rakibul", age: 25 });
// This mutates the SAME object — React won't re-render!
user.age = 26;
setUser(user); // same reference, React ignores it
// GOOD — immutable update creates new reference
setUser({ ...user, age: 26 }); // new object, React re-renders
Object.freeze (Shallow)
Object.freeze makes an object's properties non-writable and non-configurable. But it's shallow — nested objects are NOT frozen:
const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
headers: {
"Content-Type": "application/json"
}
};
Object.freeze(config);
config.apiUrl = "https://hacked.com"; // silently fails (strict mode: TypeError)
console.log(config.apiUrl); // "https://api.example.com" — unchanged
// BUT — nested objects are still mutable!
config.headers["Authorization"] = "Bearer stolen-token";
console.log(config.headers.Authorization); // "Bearer stolen-token" — MUTATED!
Deep Freeze Implementation
To truly freeze an object, you need to recursively freeze all nested objects:
function deepFreeze(obj) {
// Get all property names (including non-enumerable)
const propNames = Object.getOwnPropertyNames(obj);
// Freeze each nested object first (bottom-up)
propNames.forEach(name => {
const value = obj[name];
if (value && typeof value === "object" && !Object.isFrozen(value)) {
deepFreeze(value);
}
});
return Object.freeze(obj);
}
const config = deepFreeze({
apiUrl: "https://api.example.com",
headers: {
"Content-Type": "application/json",
nested: { deep: "value" }
}
});
config.headers["Authorization"] = "hacked"; // silently fails
config.headers.nested.deep = "changed"; // silently fails
console.log(config.headers.nested.deep); // "value" — safe!
Frozen vs Sealed vs const
These three concepts are commonly confused:
// --- const ---
// Prevents reassignment of the VARIABLE, not the value
const arr = [1, 2, 3];
// arr = [4, 5, 6]; // TypeError: Assignment to constant variable
arr.push(4); // works! Array is mutated
console.log(arr); // [1, 2, 3, 4]
// --- Object.seal ---
// Prevents adding/removing properties, but existing properties CAN be modified
const sealed = { a: 1, b: 2 };
Object.seal(sealed);
sealed.a = 99; // works! Can modify existing props
sealed.c = 3; // silently fails — can't add new props
delete sealed.b; // silently fails — can't remove props
console.log(sealed); // { a: 99, b: 2 }
// --- Object.freeze ---
// Prevents adding/removing/modifying properties (sealed + read-only)
const frozen = { a: 1, b: 2 };
Object.freeze(frozen);
frozen.a = 99; // silently fails — can't modify
frozen.c = 3; // silently fails — can't add
delete frozen.b; // silently fails — can't remove
console.log(frozen); // { a: 1, b: 2 }
| Feature | const | Object.seal | Object.freeze |
|---|---|---|---|
| Prevents reassignment | Yes | No | No |
| Prevents adding props | No | Yes | Yes |
| Prevents deleting props | No | Yes | Yes |
| Prevents modifying props | No | No | Yes |
| Shallow only | N/A | Yes | Yes |
Spread-Based Immutable Updates
The most common pattern for immutable updates in React:
// Updating a nested object immutably
const state = {
user: {
name: "Rakibul",
address: {
city: "Dhaka",
zip: "1200"
}
},
posts: [
{ id: 1, title: "Hello" },
{ id: 2, title: "World" }
]
};
// Update nested property — must spread at every level
const newState = {
...state,
user: {
...state.user,
address: {
...state.user.address,
city: "Chittagong" // only this changes
}
}
};
// Update an item in an array immutably
const updatedPosts = {
...state,
posts: state.posts.map(post =>
post.id === 2 ? { ...post, title: "Updated" } : post
)
};
// Remove from array immutably
const removedPost = {
...state,
posts: state.posts.filter(post => post.id !== 1)
};
Immer Library Concept (produce/draft)
Immer lets you write "mutative" code that produces immutable results. The concept:
// Without Immer — verbose spread nesting
const newState = {
...state,
user: {
...state.user,
address: {
...state.user.address,
city: "Chittagong"
}
}
};
// With Immer — write mutations on a draft, get immutable result
import { produce } from "immer";
const newState = produce(state, draft => {
// 'draft' is a proxy — mutations are recorded, not applied
draft.user.address.city = "Chittagong";
draft.posts.push({ id: 3, title: "New Post" });
});
// state is unchanged, newState is a new immutable object
console.log(state.user.address.city); // "Dhaka"
console.log(newState.user.address.city); // "Chittagong"
How Immer works internally:
- Creates a Proxy around the original state (the "draft")
- Records all mutations made to the draft
- Produces a new object with only the changed paths copied (structural sharing)
- Unchanged subtrees reuse the same references
Structural Sharing
Structural sharing means only copying the parts of the data structure that changed, while reusing unchanged parts:
const original = {
a: { x: 1 },
b: { y: 2 },
c: { z: 3 }
};
// Only update 'b' — 'a' and 'c' are shared by reference
const updated = {
...original,
b: { ...original.b, y: 99 }
};
console.log(updated.a === original.a); // true — same reference (shared)
console.log(updated.c === original.c); // true — same reference (shared)
console.log(updated.b === original.b); // false — new object (changed)
// This is how React (and Redux) efficiently detect changes:
// Only re-render components whose data reference changed
Common Mistakes
- Saying
constmakes an object immutable.constonly forbids reassignment of the binding — you can still mutate the object's properties all day (obj.prop = x,arr.push(y)). Immutability requiresObject.freeze(shallow) or a deep freeze. - Calling
Object.freezeon a nested config and assuming the whole tree is locked. Freeze is shallow —config.headers.Authorization = "..."still succeeds. Use a recursivedeepFreezeor a library. - Mutating React state directly (
state.user.age = 26) and then callingsetState(state). The reference hasn't changed, so React's===comparison says "nothing happened" and skips the re-render. Always create a new reference via spread or Immer.
Interview Questions
Q: What's the difference between Object.freeze, Object.seal, and const?
constprevents variable reassignment but allows object mutation.Object.sealprevents adding or removing properties but allows modifying existing ones.Object.freezeprevents all modifications — it's sealed plus read-only. Both seal and freeze are shallow — nested objects are not affected.
Q: Why does immutability matter in React?
React uses reference equality (
===) to detect state changes. If you mutate an object, its reference stays the same, so React thinks nothing changed and skips re-rendering. Immutable updates create new object references, which React detects as changes and triggers re-renders.
Q: What is structural sharing?
When making an immutable update, you only create new objects for the changed parts. Unchanged subtrees keep their original references. This makes immutable updates memory-efficient — you're not deep-cloning the entire state, just the path that changed.
Q: What's the difference between Object.freeze and Object.seal? (short)
freezemakes properties non-writable AND non-configurable (can't modify, add, or delete).sealmakes properties non-configurable but still writable (can modify existing, but can't add or delete). Both are shallow.
Q: Does const make an object immutable?
No.
constonly prevents reassignment of the variable. The object itself is still mutable — you can add, modify, and delete properties. UseObject.freezefor immutability.
Quick Reference — Cheat Sheet
IMMUTABILITY — QUICK MAP
const -> prevents REASSIGNMENT, NOT mutation
Object.seal -> no add / no delete, CAN modify existing
Object.freeze -> no add / no delete / no modify (SHALLOW!)
deepFreeze -> recursive Object.freeze over all nested objects
React rule:
new reference -> React re-renders
same reference + mutated -> React SKIPS re-render
Immutable updates:
spread -> { ...state, key: newValue }
nested spread -> spread at every level of the path
array update -> state.arr.map / filter / slice / [...arr]
array add (no push) -> [...arr, item]
array remove -> arr.filter(...)
Immer `produce` -> write mutations on draft, get new object
Structural sharing:
only changed path is copied; unchanged subtrees
reuse the SAME reference (memory-efficient)
Previous: Polyfills -> Write Your Own map, bind, and Promise Next: Pass by Value vs Pass by Reference -> Text Message vs Google Docs Link
This is Lesson 14.4 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.