Embracing Immutability in JavaScript: The Power of Immutable Data Structures

2022-12-14

#development#frontend
Embracing Immutability in JavaScript: The Power of Immutable Data Structures

You change an object in one function. Three screens away, something breaks. You didn't touch that code. You didn't import that module. But the object you mutated was shared by reference, and now you're debugging a ghost.

This is what mutability costs you. Not always, not loudly — but reliably, at the worst possible time.

Predictability Is Not Optional

When you mutate data, any code that holds a reference to that data is affected. You have to track every reference, every function that touches it, every component that reads it. That's a mental overhead that scales linearly with your codebase and exponentially with your team size.

Consider the difference:

let numbers = [1, 2, 3];
numbers.push(4);
console.log(numbers); // [1, 2, 3, 4]

You just changed numbers for everyone who references it. If another module cached this array, it now has a value it didn't expect. Compare:

const numbers = [1, 2, 3];
const newNumbers = [...numbers, 4];
console.log(newNumbers); // [1, 2, 3, 4]

numbers is untouched. newNumbers is a separate value. No side effects, no surprises. You can reason about each variable in isolation.

Debugging Becomes Trivial

When data can't change, you don't have to ask "who changed this?" The answer is nobody. The value you see is the value that was created.

let user = { name: 'Alice', age: 25 };
user.age = 26;
console.log(user); // { name: 'Alice', age: 26 }

If user is shared across your app and age is wrong, you have to trace every line that touches it. Now compare:

const user = { name: 'Alice', age: 25 };
const updatedUser = { ...user, age: 26 };
console.log(updatedUser); // { name: 'Alice', age: 26 }

user never changes. updatedUser is a new object. If something is wrong with updatedUser, you know exactly where it was created.

React Depends on It

This isn't just philosophy. React's rendering model assumes immutability. When you update state, React uses shallow comparison to detect changes. If you mutate an existing object instead of creating a new one, React doesn't see the change. Your UI doesn't update. You file a bug against React. React is fine — you mutated.

const book = { title: 'The Great Gatsby', author: 'F. Scott Fitzgerald' };
const myBook = book;
myBook.author = 'Scott Fitzgerald';
console.log(book.author); // Scott Fitzgerald

You changed myBook, but book changed too. They're the same object. In React, this means your component won't re-render because the reference didn't change. The fix:

const book = { title: 'The Great Gatsby', author: 'F. Scott Fitzgerald' };
const myBook = Object.assign({}, book, { author: 'Scott Fitzgerald' });
console.log(book.author); // F. Scott Fitzgerald
console.log(myBook.author); // Scott Fitzgerald

Two separate objects. React sees a new reference and re-renders. The original data is preserved.

Every .push() is a mutation. Every direct property assignment is a mutation. Every let where a const would work is an invitation for something to change that shouldn't.

The code that never changes your data is the code that never surprises you.