In this guide, we'll learn how to write immutable code using Immutable.js. Immutability is a critical aspect of writing accurate code and must be implemented by teams working on large codebases. We'll see how immutability can be beneficial and take a look at Immutable.js and its Map data structure. But before that, let's take a look at why is it necessary to write immutable code.
Mutation, in general, means a change from an original form. If something is mutable, it can be changed. The same principle applies to JavaScript when you change or add a property to an existing object.
Consider the below example.
1const Person = { name: "John", age: 32 };
2
3console.log("Before mutation - ", Person);
4
5Person.age = 35;
6
7console.log("After mutation - ", Person);
When we run this code in the console, we get the following output:
As you can see, the age property of the Person
object was changed, and there's no way we can get back the original object. This is called a mutation.
Similarly, mutations can be done on an array.
1const languages = ["JavaScript", "PHP", "Java", "C#"];
2
3console.log("Before mutation - ", languages);
4
5languages.pop();
6
7console.log("After mutation - ", languages);
An object is said to be immutable when its properties cannot be changed once initiated. Unfortunately, JavaScript does not have immutable data structures natively, and it's up to the programmer to write immutable code.
To demonstrate this, I'm going to rewrite the above code in an immutable manner.
1const Person = { name: "John", age: 32 };
2
3console.log(Person);
4
5const MutatedPerson = Object.assign({}, Person, { age: 35 });
6
7console.log(Person);
8console.log(MutatedPerson);
The Object.assign()
method combines two or more objects into a single one. The first parameter is the target object to which you want to combine the objects. And the rest of the parameters are the source object, each overriding the other source.
This way, we can track the values of an object and make it easier for ourselves to debug the code. But this practice doesn't guarantee that someone else could not mutate the object. That's why it is vital to use immutable data structures; hence, Immutable.js was built by the Facebook Team.
Immutable.js is a library that creates a collection of data, which, once initialized, cannot be changed or mutated.
This is how Immutable.js describes Immutable data:
Immutable data cannot be changed once created, leading to much simpler application development, no defensive copying, and enabling advanced memorization and change detection techniques with simple logic. Persistent data presents a mutative API which does not update the data in-place, but instead always yields new updated data.
The data collections are based on JavaScript's Array
, Map
, and Set
objects, but with a significant difference that any mutation is done on a copy of the original object.
For example, the set()
method, which sets the property of a Map
data collection, actually creates a new Map
collection and mutates the property, leaving the original Map
collection untouched.
Let's take a look at the Map
data structure in more detail.
An Immutable Map
is an unordered collection of key-value pairs that are based on JavaScript's object.
Let's create a very basic Map
collection.
1import { Map } from "immutable";
2
3const myMap = Map();
We import Map
constructor from immutable
, then call the constructor and assign it to a variable. This is how a very basic Map
can be created without any properties or key-value pairs.
To add properties, we pass an object of key-value pairs in the Map()
function.
1import { Map } from "immutable";
2
3const Person = Map({ name: "John", age: 32 });
To change the property, the Map
collection has a set()
method, which takes in a key as the first argument and the value as the second, and returns a new Map
collection without mutating the original collection.
1const MutatedPerson = Person.set("age", 35);
Now, if we console log the above collections, we can see two different values.
1console.log(Person);
2console.log(MutatedPerson);
There are several ways to merge Maps
.
We can merge Map
collections using the merge()
method. This returns a copy of the collection with the remaining collections merged in.
1import { Map, merge } from "immutable";
2
3const Person = Map({ name: "John", age: 32 });
4
5const MutatedPerson = merge(Person, { age: 35 });
6
7console.log(Person);
8console.log(MutatedPerson);
We get the same results as the previous example when we run the above code.
The merge()
method can also be used to merge Map
collections.
1import { Map, merge } from "immutable";
2
3const Map1 = Map({ a: 29, b: 88, c: 56 });
4
5const Map2 = Map({ a: 38, b: 45, z: 99 });
6
7const MergedMap = merge(Map1, Map2);
8
9console.log("Map1 - ", Map1);
10console.log("Map2 - ", Map2);
11console.log("MergedMap - ", MergedMap);
As you can see, the properties of the preceding Map
collection were overridden by the following collection, and the unique key-value pairs got added to the merged Map
collection. Once again, the original Map
collections were not mutated and are untouched.
The mergeWith()
function allows us to handle the existing key-value pairs by passing in a merger function as the first parameter, rather than just overriding the existing value.
For example, let's say we have to add the values of merging keys.
1import { Map, mergeWith } from "immutable";
2
3const Map1 = Map({ a: 29, b: 88, c: 56 });
4
5const Map2 = Map({ a: 38, b: 45, z: 99 });
6
7const MergedMap = mergeWith(
8 (oldValue, newValue) => oldValue + newValue,
9 Map1,
10 Map2
11);
12
13console.log("Map1 - ", Map1);
14console.log("Map2 - ", Map2);
15console.log("MergedMap - ", MergedMap);
The mergeDeep()
function is similar to the merge()
function except it can merge the collections recursively.
For example, consider the below maps.
1const Map1 = Map({ a: 29, b: 88, c: 56, d: { da: 77 } });
2
3const Map2 = Map({ a: 38, b: 45, z: 99, d: { db: 96 } });
If we use the merge()
function, the key d
will simply be overridden.
1const Map1 = Map({ a: 29, b: 88, c: 56, d: { da: 77 } });
2
3const Map2 = Map({ a: 38, b: 45, z: 99, d: { db: 96 } });
4
5const MergedMap = merge(Map1, Map2);
6
7console.log("MergedMap - ", MergedMap);
Now let's take a look when we use the mergeDeep()
function.
1const Map1 = Map({ a: 29, b: 88, c: 56, d: { da: 77 } });
2
3const Map2 = Map({ a: 38, b: 45, z: 99, d: { db: 96 } });
4
5const MergedMap = mergeDeep(Map1, Map2);
6
7console.log("MergedMap - ", MergedMap);
The values of the key d
were merged to form a new value that is assigned in the MergedMap
.
Immutable.js is a great library to use when building complex web applications. Merging Map
collections is very easy with immutable functions like merge()
, mergeWith()
, and mergeDeep()
. These methods come very handy when dealing with API responses.
So, that's it from this guide. If you have any queries regarding this topic, feel free to contact me at CodeAlphabet. Happy coding and cheers!