Should you care about immutability in Javascript?

In JavaScript, there are 6 primitive data types (string, number, boolean, undefined, null and symbol) which are immutable. By immutable we mean that these types cannot be modified. Sure! But we want to experiment, right? So let’s try mutating a string to see what happens.

let name = 'Peter';
name[2] = 'e';

console.log(name);

When we create a string literal, all of the String object methods are readily available to us. By using the bracket notation we attempt at changing the third letter in the string. We end up getting the original value back with no changes. Nice – immutability at its best!

Immutability is a concept that simply means that once an object has been created it cannot be changed. By default, however, objects and arrays in JavaScript are mutable meaning that they, in fact, can easily be changed.

const shape = {
  type: "circle",
  colour: "red"
};

const anotherShape = shape;
anotherShape.type = 'square';

console.log(shape) // result {type: "square", colour:"red"}

We create a shape object and set a property type on it. We then create anotherShape object and assign it the value of shape. Finally, we set anotherShape‘s property type to square. Upon printing out the contents of shape, we discover that it has been modified. This can become quite dangerous.

In JavaScript objects and arrays are passed by reference, not by value. So we are actually passing a reference of shape to anotherShape. By doing so we point to the same reference, same location in memory. Any modifications to either of the objects will have a knock-on effect on both. This makes our data unpredictable.

In order to avoid mutating data, the simplest thing we can do is to create a copy of the original object instead of mutating it.

Applying the ...(spread) operator:

const shape = {
  type: "circle",
  colour: "red"
};

const anotherShape = {
  ...shape,
  type: "square"
};

console.log(shape) // result {type: "circle", colour: "red"}

Using Object.assign() method:

const shape = {
  type: "circle",
  colour: "red"
};

const anotherShape = Object.assign({}, shape, {type: "square"});

console.log(shape) // result {type: "circle", colour: "red"}

We clone the shape object and merge with the type property. Essentially, we apply an override to shape‘s type property by setting it to square. Upon printing we can verify that shape has indeed not been modified.

What are the downsides of mutating objects?

  • Mutation itself is a side effect, it makes our code unpredictable and harder to test
  • It makes it very difficult to manage application state since an object can get mutated undesirably
  • We are not keeping a copy of the previous state, therefore, there is no state change history and time-travel debugging is not possible

How can making objects immutable help us?

  • By making our objects immutable we apply a stricter control and get back more predictable data (once you receive the object you can be sure that it won’t ever get mutated by a process without your knowledge)
  • Better encapsulation makes it easier to test
  • We can travel between changes in the state thus enabling time-travel debugging (Redux Devtools), we can create apps/tools that require us to go back-and-forth between the state (undo/redo functionality, etc.)

If you ever need to use immutable data structures such as maps, lists and others then you should use a library such as immutable.js. This library provides a number of immutable data structures and helper methods. Another great library is Immer. Immer exposes a default function that operates on an object or an array and produces the next immutable result. The benefit of this is that you don’t have to learn an entire new library.

Thanks!