Skip to content

How not to Make your Projects Succeed with ImmutableJS! - Part 1

April 16, 2018Jérémy Dardour7 min read

ImmutableJS

When I first learned about ImmutableJS I was convinced it was a great addition to any project. So I tried to make it a working standard at Theodo.
As we offer scrum web development teams for various projects going from POC for startups to years lasting ones for banks, we bootstrap applications all the time and we need to make a success of all of them!

So we put a strong emphasis on generalizing every finding and learning made on individual projects. With this objective in mind, we are determining which technical stack would be the best for our new projects. Each developer contributes to this effort by improving the company's technical stack whenever they make new learning.

When React was chosen as our component library we were just embarking on a hard journey that you may have experienced: making the dozens of other decisions that come with a React app.

React then what?

One of those choices leads us to choose redux as our global state management lib. We noticed that people were having troubles with Immutability in reducers, one of the three core principles of Redux

The first possibility to answer this purpose is defensive copying with the ES6 spread operator { ...object }.

The second option we studied is Facebook's ImmutableJS library.

immutable

Using ImmutableJS in your react-redux app means that you will no longer use JS data structures (objects, arrays) but immutable data structures (i.e. Map, List, ...) and their methods like .set(), .get(). Using .set() on a Map, the Immutable equivalent of objects returns a new Map with said modifications and does not alter the previous Map.

In order to make an informed choice, I observed the practices on projects that used either one of the two, gathered their issues and tried to find solutions for them. This article is the result of this study and hopefully, it will help you choose between those two!

In this first part I will compare those two options in the light of 3 criteria out of the 5 I studied: readability, maintainability and learning curve.
The second part of this study, coming soon, will explore performance and synergy with typing tools.

First Criterion, Readability: Immutable Wins!

If your state is nested and you use spread operators to achieve immutability, it can quickly become unreadable:

function reducer(state = defaultState, action) {
  switch (action.type) {
    case 'SET_HEROES_TO_GROUP':
      return {
        ...state,
        guilds: {
          ...state.guilds,
          [action.payload.guildId]: {
            ...state.guilds[action.payload.guildId],
            groups: {
              ...state.guilds[action.payload.guildId].groups,
              [action.payload.groupId]: {
                ...state.guilds[action.payload.guildId].groups[action.payload.groupId],
                action.payload.heroes,
              },
            },
          },
        },
      };
  }
}

If you ever come across such a reducer during a Code Review there is no doubt you will have troubles to make sure that no subpart of the state is mutated by mistake.

Whereas the same function can be written with ImmutableJS in a much simpler way

function reducer(state = defaultState, action) {
  switch (action.type) {
    case 'SET_HEROES_TO_GROUP':

      return state.mergeDeep(
        state,
        { guilds: { groups: { heroes: action.payload.heroes } } },
      ).toJS();
  }
}

Conclusion for Readability

ImmutableJS obviously wins this criterion by far if your state is nested.

One countermeasure you can take is to normalize your state, for example with normalizr. With normalizr, you never have to change your state on more than two levels of depth as shown on below reducer case.

// Defensive copying with spread operator
case COMMENT_ACTION_TYPES.ADD_COMMENT: {
  return {
    ...state,
    entities: { ...state.entities, ...action.payload.comment },
  };
}

// ImmutableJS
case COMMENT_ACTION_TYPES.ADD_COMMENT: {
  return state.set('entities', state.get('entities').merge(Immutable.fromJS(action.payload.comment)));
}

Second Criterion, Maintainability and Preventing Bugs: Immutable Wins Again!

A question I already started to answer earlier is: Why must our state be immutable?

  • Because redux is based on it
  • Because it will avoid bugs in your app

If for example, your state is:

const state = {
  guilds: [
    // list of guilds with name and heroes
    { id: 1, name: 'Guild 1', heroes: [/*array of heroes objects*/]},
  ],
};

And your reducer case to change the name of a guild is:

switch (action.type) {
  case CHANGE_GUILD_NAME: {
    const guildIndex = state.guilds.findIndex(guild => guild.id === action.payload.guildId);

    const modifiedGuild = state.guilds[guildIndex];
    // here we do a bad thing: we modifi the old Guild 1 object without copying first, its the same reference
    modifiedGuild.name = action.payload.newName;

    // Here we do the right thing: we copy the array so that we do not mutate previous guilds
    const copiedAndModifiedGuilds = [...state.guilds];
    copiedAndModifiedGuilds[guildIndex] = modifiedGuild;

    return {
      ...state,
      guilds: copiedAndModifiedGuilds,
    };
  }
}

After doing this update, if you are on a detail page for Guild 1, the name will not update itself!

The reason for this is that in order to know when to re-render a component, React does a shallow comparison, i.e. oldGuild1Object === newGuild1Object but this only compares the reference of those two objects. We saw that the references are the same hence no component update.

An ImmutableJS data structure always returns a new reference when you modify an object so you never have to worry about immutability.
Using spread operators and missing one level of copy will make you waste hours looking for it.

Another important issue is that having both javascript and Immutable objects is not easily maintainable and very bug-prone. As you cannot avoid JS objects, you end up with a mess of toJS and fromJS conversions, which can lead to component rendering too often.

When you convert an Immutable object to JS with toJS, it creates a new reference even if the object itself has not changed, thus triggering component renders.

Conclusion for Maintainability

Immutable ensures you cannot have immutability related bugs, so you won't have to check this when coding or during Code review.

One way to achieve the same without Immutable would be to replace the built-in immutability with immutability tests in your reducers.

it('should modify state immutably', () => {
  const state = reducer(mockConcatFeedContentState, action);

  // here we check that all before/after objects are not the same reference -> not.toBe()
  expect(state).not.toBe(mockConcatFeedContentState);
  expect(state.entities).not.toBe(mockConcatFeedContentState.entities);
  expect(state.entities['fakeId']).not.toBe(mockConcatFeedContentState.entities['fakeId']);
});

But making sure that your team-mates understand and always write such tests can be as painful as reading spread operators filled reducers.

My opinion is that Immutable is the best choice here on the condition that you use it as widely as possible in your app, thus limiting your use of toJS.

Third Criterion, Learning Curve: One point for Spread Operators

One important point when assessing the pros and cons of a library/stack is how easy will it be for new developers to learn it and to become autonomous on the project.

The results of my analysis on half a dozen projects using is that learning ImmutableJS is hard work. You have a dozen data structures to choose from, about two dozen built-ins or methods that sometimes do not behave the same way javascript methods do.

Below are some examples of such differences:

const hero = {
  id: 1,
  name: 'Superman',
  abilities: ['Laser', 'Super strength'],
}

const immutableHero = Immutable.fromJS(hero); // converts objects and arrays to the ImmutableJS equivalent


// get a property value
hero.abilities[0] // 'Laser'
immutableHero.get('abilities', 0) // 'Laser'

// set a property value
hero.name = 'Not Superman'
immutableHero.set('name', 'Not Superman')

immutableHero.name = 'Not Superman' // nothing happens!

// Number of elements in an array / Immutable equivalent
hero.abilities.length // 2
hero.get('abilities').size // 2

// Working with indexes
const weaknessIndex = hero.abilities.indexOf('weakness') // -1
hero.abilities[weaknessIndex] // throws Error

const immutableWeaknessIndex = immutableHero.get('abilities').indexOf('weakness') // -1
immutableHero.get('abilities').get(weaknessIndex) // 'Super strength'

While you can use all the knowledge you have on javascript and ES6, if you go with ImmutableJS you'll have to learn some things from the start.

Nicolas, a colleague of mine once came to me with a strange issue.
They were using normalizr and had a state that looked like the Immutable equivalent of this:

{
  fundState: {
    fundIds: // a list of fund ids
    fundsById: // an object with a fund id as key and the fund data as value: { fund1Id: fund1 },
  }
}

Their problem was that their ids, indexing funds in fundsById Map where strings of numbers and not numbers. At least twice, one of their developers had a hard time writing a feature because they were trying to get the funds like this: state.get('fundState', 'fundsById', 3) to get the fund of id 3.

The issue here is that contrary to javascript, strings of numbers and numbers are not at all interchangeable (it may be a good thing but it is an important difference!). So they had to convert all their id keys to the right type.

Another issue that colleagues shared with me was that ImmutableJS is really hard to debug in the console as shown below with our immutableHero object from above:

immutable-hero-console-no-formatter

As you can see, it's nearly unreadable and it's only a really simple object!

A great solution I encountered when trying to help them is immutable formatter a chrome extension that turns what you saw into this beauty:

immutable-hero-console-formatted

To enable it, you have to open chrome dev tools. Then access the dev tools settings and check "enable custom formatter" option:

Capture d’écran 2018-04-16 à 18.33.10

In the case of ES6, new developers have three things to learn:

  • Understand why immutability is important and why they should bother
  • How to use spread operators to enforce immutability
  • Not to use object.key = value to modify their state

Conclusion for Learning Curve

Overall the learning curve for spread operators, an ES6 tool is rather easy since you can still use all the javascript you know and love but you must be careful to the points listed above.

ImmutableJS, on the other hand, will be much harder to learn and master.

Conclusion for Part 1

In conclusion, this first part showed us that ImmutableJS comes with a lot of nice things, allows you to concentrate on working on value-added work rather than trying to read horrible reducers or looking for hidden bugs.
This, of course, is at the cost of the steeper learning curve of a rich API and some paradigms different from what you are used to!

In part II of this article, I will compare both solutions in the light of Performance and compatibility with typing.

If you liked this article and want to know more, follow me on twitter so you know when the second part is ready :).
@jeremy_dardour