JavaScript
Article

Immutability in React

By Christian Johansen

In my previous article I wrote about the hows and whys of immutability. I argued that one area where immutability truly shines is in tracking changes, one of the big challenges in modern front-end frameworks. In this article I will give you an example of how immutability can be used with React, a library developed by Facebook.

This article won’t discuss the basic of React. If you need an introduction to this library, you can read the article Introduction to the React JavaScript Library.

How to use immutability in React

In my previous article, I implemented the core game logic for Minesweeper. In this article, we will take a look at the UI layer. There’s a couple of gotchas we need to account for when using immutable.js objects with React. The first one is that you cannot pass an immutable map or list directly to a React component like this:

var data = Immutable.Map();
React.render(MyComponent(data), element);

The reason that this doesn’t work is that React copies the contents of this object property by property and merges them with the existing props. This means that React won’t get the immutable instance. It also won’t get the data contained in the map because it isn’t exposed as properties of the object – you have access the data with the get() method.

The solution is straightforward:

var data = Immutable.Map();
React.render(MyComponent({data: data}), element);

In this article, all components will work with immutable data, so we’ll create a small wrapper to avoid repeating this over and over:

function createComponent(def) {
  var component = React.createFactory(React.createClass(def));
  return function (data) {
    return component({data: data});
  };
}

With this component we can forget about the wrapper until we need to access it in the render() function (where we’ll need to refer to it as this.props.data). Our components will only define the render() function, so we can make our wrapper do even some more work for us:

function createComponent(render) {
  var component = React.createFactory(React.createClass({
    render: function () {
      return render(this.props.data);
    }
  }));

  return function (data) {
    return component({data: data});
  };
}

With this in place, defining and using components that work with immutable data is a breeze:

var div = React.DOM.div;

var Tile = createComponent(function (tile) {
  if (tile.get('isRevealed')) {
    return div({className: 'tile' + (tile.get('isMine') ? ' mine' : '')},
               tile.get('threatCount') > 0 ? tile.get('threatCount') : '');
  }

  return div({
    className: 'tile'
  }, div({className: 'lid'}, ''));
});

If the tile is revealed, we render a mine if one is there; otherwise we render the number of nearby mines, except if that number is 0. If the mine isn’t revealed, we just render it with a “lid”, which the CSS will make look like a clickable tile.

The rest of the React components are similarly simple. There is just one more snag to be aware of. React components can take an array of child components. We must make sure that immutable lists are converted to arrays before handing them off to React:

var Row = createComponent(function (tiles) {
  return div({className: 'row'}, tiles.map(Tile).toJS());
});

Calling map() on an immutable list produces a new immutable list, and toJS() returns an array representation that React can work with. These, and the rest of the UI components can be seen in full in this codepen where you can also play the game.

Speeding things up

In my previous article I mentioned that tracking changes can be drastically improved because we can short-circuit the expensive diffing algorithm in libraries like React. When you give React some new data, it calls the shouldComponentUpdate() function on all the components. If this function returns false, React won’t diff this component with the existing version, so the library won’t re-render the elements that make the component. This potentially saves a lot of work, and can lead to a massive improvement in performance.

Let’s consider our game. When you reveal a tile, the whole game is rendered over again. However, thanks to our immutable data model, all the tiles that didn’t change will still be the same exact references. These don’t need to be re-rendered, because with immutable data, same reference means no change. To let React know about this detail, we can improve our component wrapper as follows:

function createComponent(render) {
  var component = React.createFactory(React.createClass({
    shouldComponentUpdate: function (newProps, newState) {
      // Simplified, this app only uses props
      return newProps.data !== this.props.data;
    },

    render: function() {
      return render.call(this, this.props.data);
    }
  }));

  return function (data) {
    return component({data: data});
  };
}

This simple code is enough to take our app from being significantly slower than one based on mutable data, to being almost twice as fast. That’s how much of an impact efficient change tracking makes! This improved version is also available on CodePen. If you want to look closer at some numbers, there’s a GitHub repository as well, that includes some crude benchmarks and multiple implementations.

Reducing complexity

Arguably the biggest benefit of immutable data is how it reduces accidental complexity. Mutable data is inherently more complex than immutable data, because it entangles state and time. In a mutable data, time is a built-in factor. In fact, accessing the value at two different points in time is likely to give you two different values. Immutable data on the other hand, doesn’t have this feature. If you retrieve the value of an immutable data at two different points in time, it’s guaranteed that you’ll obtain the same value. This forces us to take a more conscious stance on how our data changes over time.

With immutable data, certain kinds of features that are hard, or even impossible with mutable data, become trivial to implement. One example of such feature is app-wide undo. If your application state can be represented with an immutable value, implementing undo is a matter of keeping a list of versions of the application state, and providing a button to reset the application state to the previous version.

Let’s add an “undo” feature to our Minesweeper project. Whenever the user clicks a tile, we get a new game object to render. In order to support this feature, we’ll keep the old ones around. The code to implement this feature is listed below:

var newGame = revealTile(game, tile);

if (newGame !== game) {
  gameHistory = gameHistory.push(newGame);
  game = newGame;
}

render();

Then there is the “Undo” button:

var UndoButton = createComponent(function () {
  return React.DOM.button({
    onClick: function () {
      channel.emit('undo');
    }
  }, 'Undo');
});

The channel object is an event emitter, so we need a code that listens for this event. Unless we’ve exhausted the history, we pop off the last version, and reinstate the penultimate version as the current state and re-render the game. This is done with the following code:

channel.on('undo', function () {
  if (history.size > 1) {
    gameHistory = gameHistory.pop();
    game = gameHistory.last();
    render();
  }
});

Very easy, isn’t it? Can you even imagine how you’d do something like this in an app whose state is made up of mutable objects? Play Minesweeper with undo (which is basically cheating, but hey…) by accessing the CodePen I prepared or with the demo below:

See the Pen qdObZy by SitePoint (@SitePoint) on CodePen.

Remember that I mentioned “structural sharing” in the previous article? Structural sharing means that storing two near identical versions of the application state will not actually store two copies of all the same state. This is again enabled by the fact that immutable data doesn’t change. If you were to solve the “undo” problem with mutable state, you’d have stored the mutations in some way to match up to the immutable solution in terms of memory use. Otherwise, you’d end up storing a bunch of full-blown copies, which will not scale well.

Application Snapshots

You may be thinking that most apps wouldn’t benefit in having an “undo” feature. But there are other use cases that would be interesting. For instance, if your entire app state is stored in a single immutable value, you can add snapshotting to your application just as easily. What would that be useful for? Well, saving the current state of an arbitrarily complex UI for one. Debugging is another.

Imagine that instead of getting a bug report with a bunch of ill-described steps for how to reproduce, you got a JSON string that you could dump into your browser to spin up the application in the exact same state that your use was in? This is just one more example of something that would be trivial to add to an application whose state is stored in an immutable value.

Conclusions

I’ve uploaded the source code to the previously mentioned Minesweeper implementation to GitHub where you can dive into the details. You will find an implementation using both mutable and immutable data structures, along with instructions on performing simple benchmarks. Now go forth and deploy immutability in your applications!


No Reader comments

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in JavaScript, once a week, for free.