JavaScript Interview Prep
Miscellaneous

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. Because Object.freeze is 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, and Object.freeze, how to write a proper deepFreeze, 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


Immutability thumbnail


What You'll Learn

  • Why immutability is required for React's reference-equality change detection
  • The real differences between const, Object.seal, and Object.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

  1. Predictable State: If data never changes, you always know what it contains. No mysterious mutations from distant code.
  2. Debugging: When state is immutable, you can track every change as a new snapshot — perfect for time-travel debugging (Redux DevTools).
  3. 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 }
FeatureconstObject.sealObject.freeze
Prevents reassignmentYesNoNo
Prevents adding propsNoYesYes
Prevents deleting propsNoYesYes
Prevents modifying propsNoNoYes
Shallow onlyN/AYesYes

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:

  1. Creates a Proxy around the original state (the "draft")
  2. Records all mutations made to the draft
  3. Produces a new object with only the changed paths copied (structural sharing)
  4. 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

Immutability visual 1


Common Mistakes

  • Saying const makes an object immutable. const only forbids reassignment of the binding — you can still mutate the object's properties all day (obj.prop = x, arr.push(y)). Immutability requires Object.freeze (shallow) or a deep freeze.
  • Calling Object.freeze on a nested config and assuming the whole tree is locked. Freeze is shallow — config.headers.Authorization = "..." still succeeds. Use a recursive deepFreeze or a library.
  • Mutating React state directly (state.user.age = 26) and then calling setState(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?

const prevents variable reassignment but allows object mutation. Object.seal prevents adding or removing properties but allows modifying existing ones. Object.freeze prevents 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)

freeze makes properties non-writable AND non-configurable (can't modify, add, or delete). seal makes 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. const only prevents reassignment of the variable. The object itself is still mutable — you can add, modify, and delete properties. Use Object.freeze for 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.

On this page