Tags: web-dev concept

Immutability

Date: 2026-08-17


Producing a new value instead of changing an existing one. It buys the ability to reason about what a piece of data holds without tracing every line that touched it — and that guarantee is what every modern UI framework’s change detection is built on.


Immutability means a value, once created, is never modified. Operations that appear to change it return a new value instead.

MUTABLE                  IMMUTABLE
arr.push(4)              [...arr, 4]
obj.name = 'x'           {...obj, name: 'x'}
arr.sort()               [...arr].sort()
  ↑ changes the thing      ↑ leaves it alone
    everyone else holds

The problem it solves

Shared references. Two parts of a program holding the same object means either can change it under the other:

const settings = { currency: 'GBP' }
 
renderHeader(settings)
renderBasket(settings)   // mutates it
renderFooter(settings)   // sees a different
                         // object than the
                         // header did

Nothing in the code says this happens. Finding it means reading every function that ever receives settings. Immutability removes the possibility rather than the bug — Coupling and Cohesion.

What it buys

  • Reasoning locally. A value you hold cannot change beneath you, so understanding it doesn’t require understanding everything else
  • Cheap change detection. If data never mutates, prev !== next is a complete and O(1) test for “did this change”. Deep comparison would be O(n) — this is why React, Vue and friends care
  • Undo and time travel. Keeping previous states is free when they were never overwritten
  • Safe concurrency. Nothing can be modified mid-read, so whole classes of race disappear — Race Conditions

What it costs

  • Allocation. Every “change” makes an object. More memory, more collection work — Garbage Collection
  • Verbosity, especially nested updates
  • Copies are shallow by default. The most common bug in immutable code:
// looks immutable, isn't
const next = { ...state }
next.user.name = 'Ada'   // mutates the
                         // ORIGINAL user
 
// actually immutable
const next = {
  ...state,
  user: { ...state.user, name: 'Ada' }
}

A spread copies one level. Everything below is still shared, which is why deep updates get painful and why libraries exist to do it for you.

Structural sharing

The mechanism that makes immutability affordable at scale: unchanged parts are shared between versions rather than copied.

v1   {a, b, c, d}
          │
v2   {a, b, c', d}
      ▲  ▲     ▲
      └──┴─────┴ same objects, reused
                 only c is new

Copying an object with 100 keys to change one copies 100 references, not 100 objects. That’s why the cost is far lower than it sounds, and it’s how persistent data structures work.

Where it applies without a library

  • Function arguments. Never mutate what you’re given. A function that modifies its input has a side effect its signature doesn’t declare
  • Module-level constants. const prevents reassignment, not mutation — const arr = [] still allows arr.push(). Object.freeze() is the shallow, actual thing
  • Sorting and reversing. .sort(), .reverse() and .splice() mutate in place. Copy first — Sorting and Searching
  • Dates. JavaScript’s Date is mutable and its methods mutate. A date passed around and adjusted is a reliable source of confusion

When mutation is the right answer

Local, contained mutation is fine and often clearer. Building an array in a loop inside a function that owns it, then returning it, mutates nothing anyone else can see:

function parse(rows) {
  const out = []
  for (const r of rows) out.push(transform(r))
  return out          // nobody else ever saw
}                     // `out` mutate

The rule that matters is not “never mutate” — it’s “never mutate what you don’t own.” Performance-critical inner loops are a legitimate second exception, and they should be commented as deliberate.