Skip to content

JavaScript Tutorial Functional JavaScript

Immutability Basics in JavaScript

Lesson 204 of 258 2 min read By Deepak Paswan Updated Oct 4, 2026

Immutable data is never changed after it is created. To “change” it, you create a new copy with the change applied.

JavaScript primitives (numbers, strings, booleans) are already immutable. Objects and arrays are mutable by default, so immutability is a habit you follow.

Why bother?

Shared data that can be changed from anywhere causes bugs: something changes, and you cannot tell who did it. With immutable updates, a value you hold stays exactly as it was.

script.js
JS
const original = { name: "Maya", city: "Pune" };

// mutation: the original is changed
const mutated = original;
mutated.city = "Delhi";
console.log(original.city); // → Delhi   (surprise!)

Updating objects immutably

script.js
JS
const user = { name: "Maya", city: "Pune", age: 28 };

const moved = { ...user, city: "Delhi" };          // copy with one change
const withoutAge = (({ age, ...rest }) => rest)(user); // copy without a property

console.log(user.city);        // → Pune
console.log(moved.city);       // → Delhi
console.log(withoutAge);       // → { name: Maya, city: Pune }

Updating arrays immutably

script.js
JS
const todos = [
  { id: 1, text: "Wash up", done: false },
  { id: 2, text: "Study", done: false }
];

const added = [...todos, { id: 3, text: "Cook", done: false }];
const removed = todos.filter(todo => todo.id !== 1);
const toggled = todos.map(todo => (todo.id === 2 ? { ...todo, done: true } : todo));

console.log(todos.length, added.length, removed.length); // → 2 3 1
console.log(toggled[1].done, todos[1].done);              // → true false

Notice the pattern: map replaces one item with a changed copy and keeps the others.

Nested data

Copy every level you change.

script.js
JS
const state = {
  user: { name: "Maya", prefs: { theme: "light" } },
  items: [1, 2]
};

const next = {
  ...state,
  user: {
    ...state.user,
    prefs: { ...state.user.prefs, theme: "dark" }
  }
};

console.log(state.user.prefs.theme); // → light
console.log(next.user.prefs.theme);  // → dark
console.log(next.items === state.items); // → true  (unchanged parts are shared, not copied)

Sharing the unchanged parts keeps copying cheap.

Enforcing it

Object.freeze() prevents accidental changes (Object.freeze). Remember it is shallow.

script.js
JS
const config = Object.freeze({ debug: false });
config.debug = true;        // ignored (an error in strict mode)
console.log(config.debug);  // → false

Where it matters most

  • UI frameworks (React and others) detect changes by comparing references. A new object means “something changed”.
  • Undo/redo: keep the old states.
  • Debugging: states are snapshots you can compare.

When mutation is fine

A local array you build inside a function and then return can be mutated freely. The danger is only with shared data. Do not copy huge structures in hot loops either.

The mutating methods to be careful with

push, pop, splice, sort, reverse change the array. Use their non-mutating partners: spread, filter, slice, toSorted, toReversed. See Changing Arrays Without Mutating Them.