I’ve been using Immutable.js a lot lately for my React/Redux projects. Immutable.js greatly simplifies how you think about your application’s state. Since you should never mutate your state in Redux reducers, using a library like Immutable.js is a great choice to make things a little more pleasant for yourself when dealing with complex state data. In this blogpost, I’ll talk about the two most commonly used data structures: Map and List.

It’s probably worth mentioning how the docs for Immutable.js work. Back when I first started reading them, I got super confused by the docs since its syntax is written in TypeScript. Not knowing this (in my defense, it’s not really called out anywhere), I got a bit confused. Taking some to decipher things a little, I realized it’s actually not that hard if you know what you’re looking at. I recommend checking out the Typescript handbook if its syntax is foreign to you.

Immutability and persistent data

Immutable objects cannot be modified after they are created. That doesn’t seem very interesting, since application state changes all the time, right? Luckily, Immutable.js has persistent data structures, which means that we can modify the data - with the caveat that the original value does not get modified, but that the operation returns a new value. That might sound a little confusing, but things will clear up in just a second. Hang in there!

Why would you use this?

One of the hardest parts of working on an app is keeping track of all the state changes, and making sure that you don’t modify things when you’re not supposed to. This leads to all kinds of nasty stuff, like defensive copying (hopefully) and objects being modified all over the place (more likely).

I used to work a lot with Angular before I switched to React. The two-way data binding is pretty awesome at first, until you start developing more complex applications. If you do, you’ll run into performance issues, as well as just a tangled mess of data being passed up and down through components. This makes it very hard to follow the flow of data throughout your application. In contrast, React uses one-way data binding where data flows down from above, and never the other way around.

To sum it up (and use an already overused term), it makes your data a lot easier to reason about. It’s also important to note that Immutable.js is not specific to the React ecosystem. You can use it in your jQuery app, your Angular app, a Redux app without React (a project I’m working on now is basically a tabletop card game where the application state is stored using Redux and Immutable.js on the server).

How it works (in a nutshell)

With Immutable.js, we mutate and retrieve our data using methods. This is quite a change compared to the regular JavaScript way of directly setting data on an object, or retrieving it using a key. For example, this is how we retrieve data from an Immutable.js Map:

import { Map } from 'immutable';

let person = Map({
  firstName: 'Thomas',
  lastName: 'Tuts'
});

person.firstName; // -> undefined
person.get('firstName'); // -> 'Thomas'

If the Map thing looks weird to you, read on! It’s one of the two data structures we’ll talk about today. To set data, we use one of the many ‘persistent changes’ methods:

import { Map } from 'immutable';

let person = Map({
  firstName: 'Thomas',
  lastName: 'Tuts'
});

person.set('firstName', 'John');

person.get('firstName'); // -> 'Thomas'

Wait, what? Nope, that’s not a typo. The name property is still 'Thomas', and it should be! Remember: any operation that would mutate a data structure in Immutable.js does not modify the original value, but rather returns a new value! In code:

import { Map } from 'immutable';

let person = Map({
  firstName: 'Thomas',
  lastName: 'Tuts'
});

person = person.set('firstName', 'John');

person.get('firstName'); // -> 'John'

Iterable

An Iterable is a set of key/value entries that can be iterated (e.g. using .map(), .filter(), .reduce(), …). All collections in Immutable.js, like Map and List, use Iterable as a base class. This means that all methods of an Iterable are available both on the Map and the List examples we’ll see below. All inherited methods are also shown in the Map and List docs, so you don’t need to go back and forth between tabs to see what methods are available to use.

Maps

Think of Map as an object. It stores values using keys. Let’s create a Map that represents a data structure of a book in an online store. To make things a little easier for ourselves, we’ll use the .fromJS() method provided by Immutable.js. This returns a given JS object as an Immutable.js representation, converting objects into Maps, and arrays into Lists.

import { fromJS } from 'immutable';

let book = fromJS({
  title: 'Harry Potter & The Goblet of Fire',
  isbn: '0439139600',
  series: 'Harry Potter',
  author: {
    firstName: 'J.K.',
    lastName: 'Rowling'
  }
});

Reading data

Let’s get some data! First, we’ll get the book’s title. That’s pretty easy, especially considering the initial code examples in this post used the very same syntax. Here goes:

book.get('title'); // -> 'Harry Potter & The Goblet of Fire'

Now, to make things a little more difficult, let’s get the author’s last name. Many people would reach for this solution:

book.get('author').get('lastName'); // -> 'Rowling'

This works just fine, but as you might imagine, the syntax would get pretty cumbersome and verbose if we’d like to access values that are a couple levels down. Luckily, Immutable.js provides methods for setting/getting nested data too. Neat! The syntax looks like this:

book.getIn(['author', 'lastName']); // -> 'Rowling'

Basically, any time you see a method with In behind it (e.g. .getIn(), .setIn(), .updateIn(), …), it is a method that takes either a key or a key path as an argument. A key path is usually an array that contains the path to the data you’re interested in. These keys can be regular keys or indexes (for Lists, which we’ll talk about in just a second).

Modifying data

Let’s change our author’s last name to 'Bowling'. Just like we did before when retrieving the data, we’ll use a key path to make things a little more readable. .setIn() takes two parameters: the first one is a key path (or just a key), and the second one is the value you’d like to set on that key or key path.

book.setIn(['author', 'lastName'], 'Bowling');

Time for a pop quiz! Take a moment to think about what this operation returns. Does it return the value it just set ('Bowling'), the updated author Map, or the complete book value, with its updated author?

The answer is the latter: .setIn() has modified the root value (in this case, our book data) and has returned a new value with the updated author. Keep in mind that the new data gets returned and it is not modified in our original value (remember the ‘persistent changes’ part). If we were to read the author’s last name in our book variable, it would still be 'Rowling'. You probably know the drill by now: to update the book, you need to assign the updated result of the operation to your variable:

book = book.setIn(['author', 'lastName'], 'Bowling');

There are a bunch of other methods, like .merge(), .delete(), .update(), and many others. In the interest of making this blogpost (relatively) short, we won’t discuss these here. Take a look at the documentation for the full list of operations!

Lists

Think of Lists as being similar to standard JavaScript arrays. They are ordered and indexed. With a List, we have access to operations like .push(), .pop(), .unshift(), and many other array-like methods (including the methods inherited from Iterable). Let’s make our book data a little more interesting by introducing genres and a list with prices from 4 stores:

import { fromJS } from 'immutable';

let book = fromJS({
  title: 'Harry Potter & The Goblet of Fire',
  isbn: '0439139600',
  series: 'Harry Potter',
  author: {
    firstName: 'J.K.',
    lastName: 'Rowling'
  },
  genres: [
    'Crime',
    'Fiction',
    'Adventure',
  ],
  storeListings: [
    {storeId: 'amazon', price: 7.95},
    {storeId: 'barnesnoble', price: 7.95},
    {storeId: 'biblio', price: 4.99},
    {storeId: 'bookdepository', price: 11.88},
  ]
});

(Remember, our arrays get converted to Lists automatically. The same thing happens for objects, which get converted to Maps.)

Reading data

To get a value of an index (i.e. a key) in a List, you use the .get() or .getIn() methods:

book.getIn(['genres', 1]); // -> 'Fiction'

Remember, we can mix and match keys and indexes in the key path, so to get Amazon’s price for the book, we’d use this key path:

book.getIn(['storeListings', 0, 'price']); // -> 7.95

Modifying data

Wait a minute, what’s ‘Crime’ doing in there? This is Harry Potter! Someone clearly botched the data entry. Let’s fix that by updating the right value:

book = book.setIn(['genres', 0], 'Fantasy');

Much better! Suppose we want to add an additional genre ‘Wizards’ to the book:

book = book.set('genres', book.get('genres').push('Wizards'));

You might have noticed something already: that syntax looks a little verbose. Luckily, Immutable.js collections have the .update() method. It takes two arguments: a key (or a key path in the case of .updateIn()), and an updater function. The updater function is basically a function that gets the value passed in as a parameter (similar to methods like .map() and .forEach()), in which we update the value. In our case, we’ll have access to the genres, which we will update by pushing another genre into the List:

book = book.update('genres', genres => genres.push('Wizards'));

Let’s imagine for a second that we’d need to update Amazon’s price to $6.80. This could be done using .setIn() and the right key path:

book = book.setIn(['storeListings', 0, 'price'], 6.80);

However, we usually don’t know the index of the entry we want to update. It’s pretty easy to find it using .findIndex():

const indexOfListingToUpdate = book.get('storeListings').findIndex(listing => {
  return listing.get('storeId') === 'amazon';
});
book = book.setIn(['storeListings', indexOfListingToUpdate, 'price'], 6.80);

Let’s calculate the average price of the book across bookstores. We’ll first make a sum of all the prices using .reduce(), and then divide it by the amount of store listings using .count().

book.get('storeListings')
  .reduce(
    (total, value) => total + value.get('price'),
    0
  ) / book.get('storeListings').count();

Finally, a more complex example: for some reason, all of the prices need to be reduced by 10% for all store listings. We’ll update our book prices accordingly, using a combination of .update() and .map():

book = book.update(
  'storeListings',
  storeListings => storeListings.map(listing => listing.update(
    'price',
    price => price * 0.9 
  ))
);

In a nutshell, this is what’s happening above:

  1. Update the storeListing in our book value, using an updater function that changes the storeListing value
  2. Map all of the entries in storeListing, transforming each value
  3. Update the price value of the listing being mapped, by lowering the price by 10% and returning the result

Conclusion

This blogpost already went on a lot longer than I expected, but we’ve still only scratched the surface of this awesome library. Still, hopefully these small examples have shown you how powerful Immutable.js can be for managing your data. Stay tuned for updates and more Immutable.js goodness!