Immutability in JavaScript

Carlos Cuesta - Jan 26 '21 - - Dev Community

Have you ever heard something about immutability? I'm mostly sure the answer is yes!, particularly on the programming ecosystem. Despite being a popular term, there are some misconceptions about the importance and principles of it. Let's dive in into it!

What does it mean?

Immutability is the state of not changing, or being unable to be changed.

On a programming context, means that when we need to change the state in our program, we must create and track a new value rather than mutating the existing one.

🚨 This does not mean we can't have values that change over the lifecycle of our program. That's a common misconception about immutability 🖐

Immutable 🆚 Mutable

To understand the difference between both, take a look at the following example. Imagine that we have a shopping cart 🛒 object, that contains two properties, id and total.

const shoppingCart = { id: '69zd841', total: 0 }
Enter fullscreen mode Exit fullscreen mode

Let's say that we want to update the total property of our shoppingCart, how can we achieve this?

Clone and update

Using the spread operator we can create a new object cloning the previous cart and updating the total property, while preserving our original object in a pristine condition.

{ ...shoppingCart, total: 15 }
Enter fullscreen mode Exit fullscreen mode
Mutate

Through the object property accessor we can perform a modification to our original object.

cart.total = 15
Enter fullscreen mode Exit fullscreen mode

The difference between those two examples is that on the first one we preserved our original shopping cart and we created a new one and on the second one we overwrited the value of the total property.

The benefits

Now that we understood the concept of immutability and the difference between mutable 🆚 immutable data, let's see the benefits of applying it.

Immutability is like a seatbelt, it won't prevent an accident but it saves your life when it does

Predictability

Predictable code is code where you can anticipate the outcome with a single read. Mutation hides change, as we've seen before, the source is lost. Hidden change creates unexpected side effects that can cause complex bugs 🐛.

When we can predict the outcome, we start to feel more confident with our code simplifying our thought process. This helps us reason easier about our program.

Tracking mutations

When you mutate data, you're overwriting the same reference every time that you update something as we've pointed on the first example. Then it's impossible to have a track of the changes you've done to the program state.

Immutable data gives you a clear history of state to the program, because you're creating a new reference based on the source.

The drawbacks

Whenever we start creating new values such as Array, Object etc, instead of mutating existing ones the next question always is is: What kind of impact has this for performance?.

Performance

Avoiding mutations has a cost that's correct, every time we have to reallocate a new value, that's consuming CPU time and memory, including the garbage collection process for values that are no longer referenced.

Is that an acceptable trade-off? It depends. We need some context to answer that question, for example:

If you have a single state change, that happens a few times on the lifecycle of your program, creating a new object, is certainly not a problem. It won't have no practical impact on the performance of the app. Compared to the time you will save not having to track and fix bugs caused by side effects, the winner clearly is immutability 🎉

Again, if that state change is going to occur frequently, or it happens in a critical path of your app, then performance is a totally valid concern 👍

Thankfully, there are some libraries out there that will provide performance optimizations such as Immutable.js and seamless-immutable.

Immutable data in JS

Constants

A constant is a variable that cannot be reassigned and redeclared. This is an important concept to understand, because it does not mean the value it holds is immutable.

The use of const tells the human being who's reading your code that the following variable won't be reassigned or redeclared. It's a great improvement in code readability because we're intentionally communicating that.

const fruits = ['🍌','🍓', '🥥']

// We can't reassign the constant
// ❌ TypeError: Assignment to constant variable.
fruits = ['🍏']

// We can't redeclare the constant
// ❌ SyntaxError: Identifier 'fruits' has already been declared
const fruits = ['🥝']

// ✅ We can mutate the value as we want
// fruits -> ['🍌','🍓', '🥥', '🍍']
fruits.push('🍍')
Enter fullscreen mode Exit fullscreen mode

Spread syntax

Also known as ..., it's a useful operator for cloning iterables such as Array or Object.

const fruits = ['🍌','🍓', '🥥']
const shoppingCart = { id: '69zd841', total: 0 }

// Add a some fruits to the end of the array
const fruitsCollection = [...fruits, '🍍', '🥝']

// Update the shoppingCart total and clone all the other properties
const shoppingCartWithTotal = {...shoppingCart, total: 15 }
Enter fullscreen mode Exit fullscreen mode

Object.freeze

The Object.freeze function is a simple way to turn a mutable Object or Array into an "immutable value". This function freezes the object 🥶 you pass as argument.

But what does frozen object means? A frozen object is an Object whose properties/indices has been marked as read-only and non-reconfigurable, so they can't be reassigned and the Object itself is marked as non-extensible, so no new properties can be added.

The freezing process ❄️ is only made at the top level of the object. If you want to make your whole object immutable, make sure you deep freeze each sub Object or Array 🤓.

const fruits = Object.freeze(['🍌','🍓', '🥥'])
const shoppingCart = Object.freeze({ id: '69zd841', total: 0, products: [] })

// We can't extend the fruits Array
// ❌ TypeError: Cannot add property N, object is not extensible
fruits.push('🍏')

// ❌ We can't mutate shoppingCart top-level properties
// shoppingCart -> { id: '69zd841', total: 0, products: [] }
shoppingCart.total = 123

// 🚨 We can mutate shoppingCart objects
// shoppingCart -> { id: '69zd841', total: 0, products: [{ name: 'Beer' }] }
shoppingCart.products.push({ name: 'Beer' })
Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . . . . . .