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 didNothing 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 !== nextis 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.
constprevents reassignment, not mutation —const arr = []still allowsarr.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
Dateis 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` mutateThe 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.