Immutable.js practical guide
In this blog post, I'll try to give a brief, practical and easy to understand introduction to Immutable.js library. Why should you use it, a difference between mutable and immutable code, examples in both Immutable.js and plain JavaScript. Lt's start by introducing the library.
Immutable.js
Immutable persistent data collections for Javascript which increase efficiency and simplicity.
As the name suggests, the Immutable.js library brings immutability and more precisely immutable data structures to the JavaScript. It is an open-source project created by Lee Byron from Facebook, and it has gained a lot of popularity (>10000 stars in GitHub). The increased interest in functional programming, React + Redux and Facebook may be one of the reasons for such popularity, but the overall quality of the project must have an effect on reputation. It has a clear documentation, clean and well-designed API, focus on performance and extensive unit test suite.
What are mutable and immutable objects?
... an immutable object is an object whose state cannot be modified after it is created. This is in contrast to a mutable object, which can be modified after it is created
... an object that uses memoization to cache the results of expensive computations could still be considered an immutable object. -Wikipedia
From a user perspective, everything you do using Immutable.js is guaranteed never to modify your data. For example, if you change value using myDragon.set('name', 'Blizzard')
it will create new instance.
You might wonder how cost-effective is to clone object even on tiniest change? The answer is not a black and white; true or false. Both mutable and immutable objects have their uses. In a React web app, the benefits might include the fact tracking changes is very performant with immutable data structures. On the other hand, immutable data structures in a large game with lots of changes happening in a real-time would be bad for the performance.
Let's examine what kind of issues can be avoided using immutable data structures.
Why mutation can be a problem and how to avoid it?
Let's take a built-in Array and it's methods as an example. The Array prototype contains a mixed set of methods that modifies the array and also methods that return a new array.
Sometimes it's hard to remember which of the methods mutate the original array:
var originalNames = ['Tony', 'Rachel', 'Matthew'];
function removeLastItem(arr) {
return arr.pop();
}
var withoutMatthew = removeLastItem(originalNames);
console.log(originalNames);
> ['Tony', 'Rachel']
console.log(withoutMatthew);
> ['Matthew']
Exactly opposite what we expected a function to return. Not only we returned an incorrect value, but we did an even nastier thing: we changed the input value to be incorrect!
Let's fix that using other function that Array provides:
function removeLastItem(arr) {
return arr.slice(0, -1);
}
It's not as easy to read as the .pop
version, but now it is returning a new array. Slice takes items from index zero until second last item of the array.
By using Immutable.js, you can be sure that it's methods doesn't mutate the state.
var originalNames = Immutable.Array(['Tony', 'Rachel', 'Matthew'])
function removeLastItem(arr) {
return arr.pop()
}
var withoutMatthew = removeLastItem(originalNames)
console.log(originalNames)
> ['Tony', 'Rachel', 'Matthew']
console.log(withoutMatthew)
> ['Matthew']
Immutable.js is designed to match closely to the ECMAScript spec of an Array (Map and Set also) with a crucial difference of never mutating the original array.
It's not only about arrays
Immutable.js has many data structures and one the most used one is Map. The Map is like an object literal in plain JavaScript, but with built-in immutability. Object literals in JavaScript are powerful, but Immutable.js Map takes it next level with helper methods and at the same helps you avoid some nasty bugs. Hobbit example will explain what kind of errors you might avoid.
We want to create new Hobbit based on information about a human:
var bilboHuman = { name: 'Bilbo', race: 'human' }
function makeHobbitFrom(human) {
human.race = 'hobbit'
return human
}
var bilboHobbit = makeHobbitFrom(bilboHuman)
console.log(bilboHuman);
> Object {name: "Bilbo", race: "hobbit"}
console.log(bilboHobbit);
> Object {name: "Bilbo", race: "hobbit"}
We have "lost" the human and have two hobbits running free!
To avoid losing our human we need to clone the human and then change the race of the cloned one.
With ES6, it's quite straight-forward to create a clone.
function makeHobbitFrom(human) {
var hobbit = Object.assign({}, human)
hobbit.race = 'hobbit'
return hobbit
}
Other cloning techniques can be found from StackOverflow answer.
It's annoyingly easy to make this kind of errors and by using the Immutable.js Map we can avoid such problem.
var bilboHuman = Map.fromJS({ name: 'Bilbo', race: 'human' })
function makeHobbitFrom(human) {
return human.set('race', 'hobbit')
}
var bilboHobbit = makeHobbitFrom(bilboHuman)
console.log(bilboHuman.toJS())
> Object {name: "Bilbo", race: "human"}
console.log(bilboHobbit.toJS())
> Object {name: "Bilbo", race: "hobbit"}
With one line we can create a new clone with the race set to a hobbit.
Beyond mimicking the built-in methods
The Immutable.js is doing more than just mimicking the Array method calls and returning new immutable collections.
Manipulating nested structures is one of many features that make the library appealing. Music related example that demonstrates how easy it is to manipulate data in a less error-prone way.
We're receiving bands and members from an API as a JSON, and then we convert it to JavaScript object:
{
band_1: {
name: "Ladytron"
members: {
artist_12323: {
name: "Helen Marnie"
},
artist_12324: {
name: "Mira Aroyo"
},
artist_12425: {
name: "Daniel Hunt"
},
artist_12426: {
name: "Reuben Wu",
instrument_played: "Alesis QS6.1"
}
}
}
}
A user of our application has noticed an error in the data. Reuben Wu didn't play Alesis QS6.1 in the band Ladytron; it was Korg MS-10. Let's see how the user-interface code would look like with Immutable.js.
Let's turn the received content as an Immutable.js collections.
var data = Immutable.fromJS(content)
With React + Redux it's common that we have a whole application state as one object. To update the name, we would have to create a new state with the correct artist name.
setInstrumentPlayed: function (state, bandId, artistId, instrument_played) {
return state.setIn([bandId, 'members', artistId, 'instrument_played'],
instrument_played);
}
Okay.. what just happened there? Let's examine how a developer would write a deep update in a plain JS.
setInstrumentPlayed: function (state, bandId, artistId, instrumentPlayed) {
var newstate = Object.assign({}, state)
var band = newstate[bandId]
if(!band) {
return state
}
var artist = band.members[artistId]
if(!artist) {
return state
}
artist.instrument_played = instrumentPlayed
return newstate;
}
A shorter version can be achieved using a helper function that allows us to access using, for example, dot notation ([bandId, 'members', artistId, 'instrument_played'].join('.')
).
This line has exactly same end result:
return state.setIn([bandId, 'members', artistId, 'instrument_played'],
instrument_played);
It will go deeper and deeper into the data structure, first by band id (band_1
), then members
property, and then artist id (artist_12426
) and then finally instrument_played
property. It doesn't change the state; instead, it returns new state with updated value.
Further reading
- Zsolt Nagy - Introduction to Immutable.js
- Tero Parviainen - Full-Stack Redux Tutorial Even if you're not doing React + Redux, it has nice usage and unit tests for Immutable.js data structures.
- EggHead.io video - Immutable.js: Introduction - Easing the Pains of Mutability
Let me know what you think! If you have a resource that you found useful, then please share them.