Filipe Colaquecez

Filipe Colaquecez

What does immutability really mean?

javascript, array, best practices, functional programing

It's important to understand what immutability is and the benefits that it brings to your code. :)

Immutability is one of the most important principles of functional programming. In this article, I will talk about this topic and show some examples of use and how it can help you. :)

Immutability?

An immutable value is something that can't be changed; a great example is the type string:

let country = "Brazil"

let country2 = country

console.log(country) //Brazil
console.log(country2) //Brazil

country2 = "Japan"

console.log(country) //Brazil
console.log(country2) //Japan

But... You said that strings are immutable, but in your example you changed the value >:(

So weird, right?

When we create a string, we can't change the reference, but is possible to reassign the variable to other values and those are different things. Let's see an example:

let myString = "Cinemark"
myString.toUpperCase() //CINEMARK
console.log(myString) //Cinemark

Realize that the value didn't change, the method just returns a new string: to use it, you can reassign the value or just create a new variable.

To better illustrate the point, when we do:

const a = 3
const b = a

This creates two copies in two different spaces in the memory, and it's happening because the type number is immutable.

Let's move on. I will show some examples using mutable types, and they will help you understand it better!

Primitives Types

Before continuing, we need to understand what primitive types are in JavaScript, which are: String, Number, BigInt, Boolean, Undefined, Symbol, and Null, all of which are immutable.

Remember, a variable can be reassigned with a new value, but your reference can't be changed or shared the way we do with Objects, Arrays, or Functions.

Reading recommendation: Primitives Types

Constants - Objects and Arrays

Constants can't be changed. This means that the variable can't be reassigned, but it only works for primitive types, for example:

const name = "Filipe"
name = "Filipe Pereira"
//Uncaught TypeError: Assignment to constant variable.

const age = 26
age = 27
//Uncaught TypeError: Assignment to constant variable.

An example on objects

const car = { brand: "Fiat" }
car = {}
//Uncaught TypeError: Assignment to constant variable.

We can't reassign the value of "car", but we can change directly your props:

const car = { brand: "Fiat" }
car.brand = "BMW"
console.log(car) // {brand: 'BMW'}

Side-Effects

Side-effects are changes or actions that happen out of our scope or context. This means that there are some behaviors we didn't expect to happen. The next example will clarify the issue:

const person = { name: "Filipe" }

const person2 = person

person2.name = "James"

console.log(person2.name) //James

console.log(person.name) //James

When we made the change of person2.name, it also affected our first object person.name, and that's because they share the same reference; thus, any properties that you change automatically will change all the other objects that have the same reference.

At this moment, it is clear why the primitive types are immutable. Imagine if it were possible to change the reference of the letter:

String.a = "b"

If it were possible, we would generate a side-effect in all the letters a having an undesirable behavior.

This GIF is a perfect way to explain this:

Reference x Value - Immutability

That's why, when you are working with primitive types you need to create a new reference to avoid future errors in your application.

I got it, but how to solve it?

We can use the spread operator to create a new object; this way, we'll create a new reference, so when it changes the value it doesn't change the other references:

const person = { name: "Filipe" }

const person2 = { ...person }

person2.name = "James"

console.log(person.name) //Filipe

console.log(person2.name) //James

Another example is:

const person = {
  name: "Filipe",
  role: {
    name: "dev 1",
  },
}

const person2 = { ...person }

person2.name = "Cleber"

console.log(person.name) //Filipe

console.log(person2.name) //Cleber

What if the person2 now has a new role from dev 2? Maybe you would think of something like that: person2.role.name = 'dev 2'

Let's test it:

const person = {
  name: "Filipe",
  role: {
    name: "dev 1",
  },
}

const person2 = { ...person }

person2.name = "Cleber"

person2.role.name = "dev 2"

console.log(person.name) //Filipe
console.log(person.role.name) //dev 2

console.log(person2.name) //Cleber
console.log(person2.role.name) //dev 2

Only the root of the object didn't change when we did person2.name, but the person2.role.name changed all the objects, and it happens because the spread uses the technique called "shallow cloning". It copies only the root of your object, and all the nested objects it gets have the original reference. That's why when we changed the role, we ended up changing all the objects.

To solve it, we can use a technique called "deep cloning". Basically, we clone both the root object and the nested objects:

const person = {
  name: "Filipe",
  role: {
    name: "dev 1",
  },
}

const person2 = {
  ...person,
  role: {
    ...person.role,
  },
}

person2.name = "New name"

person2.role.name = "dev 2"

console.log(person2.name) //New Name
console.log(person2.role.name) //dev 2

console.log(person.name) //Filipe
console.log(person.role.name) //dev 1

You can also use the native javascript method: structuredClone(), it will create a deep clone of a given value

const user = {name:'Filipe', techs:['Swift', 'React Native']}
const newUser = structuredClone(user)
newUser.techs.push('SwiftUI')
console.log(newUser.techs) //['Swift', 'React Native', 'SwiftUI']
console.log(user.techs) //['Swift', 'React Native']

There are a lot of libs that do it for you automatically. React recommends the immutable.js because there comes a point where it's extremely difficult to do this kind of thing and if you use redux it's worth taking a look at the redux toolkit that already uses the immer internally.

That's it guys!

As our application grows, we have a lot of states changes potentially coming from asynchronous calls, and mutability "hides" these changes because of side-effects which make the behavior of our application unstable and make it very difficult to debug.

That's all. By keeping your architecture immutable you can predict what you have in your state and be sure that nothing changes as the application runs, and it helps you to better maintain your code.

Follow me on Social Media

Instagram:

@colaquecez.dev

Share it ;)