Conceptly
← All Concepts
🔒

Immutability

FoundationsA value that, once created, is never changed in place -- updates produce new values instead

Immutability means that once a value is created, it is never changed. Instead of mutating an existing object or array, every update produces a new one. The original remains intact and any code still holding a reference to it sees the same value it always did.

Architecture Diagram

🔄 Process

Dashed line animations indicate the flow direction of data or requests

Why do you need it?

When multiple parts of a program share a reference to the same mutable object, any one of them can change it and silently affect the others. Debugging this requires tracking every write across the entire codebase, not just the code immediately in front of you. Mutation also destroys history: once a value is overwritten, the old version is gone, making rollback and undo difficult to implement correctly. In UI frameworks, mutation is particularly painful because the framework often relies on change detection to decide when to re-render, and silent in-place mutation can easily defeat that mechanism.

Why did this approach emerge?

Early programming languages were designed with memory efficiency in mind, and in-place mutation was the obvious way to update a variable without allocating new storage. As programs grew more concurrent and reactive UI became common, the tradeoffs shifted. The shared mutable state that felt efficient became a primary source of bugs that were hard to reproduce and hard to reason about. Functional languages had long enforced immutability as a core constraint, but the practice spread to mainstream languages when frameworks like React made it a practical requirement for predictable rendering.

How does it work inside?

Immutability works by treating values as permanent descriptions of state at a point in time. To update something, you create a new value that incorporates the change while leaving the original untouched. In JavaScript, the spread operator and array methods like map and filter make this pattern convenient for objects and arrays. Structural sharing -- where new values reuse the parts of old values that did not change -- makes immutability efficient even for large data structures. The key insight is that a reference check is enough to detect whether something changed, because mutation is ruled out. That makes change detection fast and reliable.

In Code

Mutable update vs immutable update

// Mutable: modifies the original object
const user = { name: "Alice", age: 30 };
user.age = 31; // original is now changed

// Immutable: produces a new object, original untouched
const user = { name: "Alice", age: 30 };
const updatedUser = { ...user, age: 31 };
// user.age is still 30
// updatedUser.age is 31

Spread creates a shallow copy with the override applied. The original reference remains stable, so any other code holding it still sees the unchanged version.

Immutable nested update in React state

type State = {
  user: { name: string; settings: { theme: string } };
};

// Correct: new references at every affected level
setState((prev) => ({
  ...prev,
  user: {
    ...prev.user,
    settings: {
      ...prev.user.settings,
      theme: "dark",
    },
  },
}));

// Wrong: mutates the existing object, React may not re-render
// state.user.settings.theme = "dark"; // never do this

React compares state by reference. A new top-level object is not enough if the nested object was mutated in place. Each level that changed must be a new reference for the update to propagate correctly.

Boundaries & Distinctions

Immutability and pure functions are closely related but not the same thing. A pure function avoids side effects; immutability is a property of the data itself. A pure function that receives a mutable object could technically mutate it and still return a value, but doing so would violate immutability and reintroduce the problems both practices are trying to prevent. In practice, the two go together: pure functions compute new values from old ones, and immutability ensures the old values are never silently overwritten in the process. Immutability is also different from constants. A constant variable cannot be reassigned, but its contents can still be mutated if it holds a reference to an object or array. Immutability goes further by preventing the contents from changing, not just the binding. In JavaScript, `const` is not immutability; `Object.freeze` is closer, and libraries like Immer enforce it through a structural copy-on-write pattern.

Trade-off

Immutability requires allocating new objects for every update instead of modifying existing ones. For large data structures or very high update frequencies, this can create memory pressure and additional garbage collection work. Deeply nested updates also require verbose spread chains unless tooling like Immer is used to hide that complexity. In performance-critical paths, such as game loops or large data processing pipelines, carefully controlled mutation can be the right tradeoff. The key is to treat mutation as an explicit, localized choice rather than the default.

When should you use it?

Immutability is most valuable wherever change detection, undo, concurrent access, or predictable data flow matters. React state is the most common example in frontend development, but the same principle applies to Redux stores, event sourcing systems, and any architecture where you need a reliable record of what the data looked like at different points in time. The practical challenge is that deeply nested structures require spreading at every affected level, which can become verbose. Libraries like Immer address this by letting you write mutation-style code that is converted into immutable updates behind the scenes. Knowing when to reach for such a library versus when plain spread is sufficient is a practical skill worth developing alongside the concept.

React state updates -- passing a new object reference to trigger re-renders reliablyUndo and time-travel -- keeping a history of past states because old values are never overwrittenConcurrent reads -- sharing data between threads or async operations without locks or racesPredictable data flow -- tracing how data changed through a system without mutation obscuring history