Building a Sequencer with React and Immutable.js

A. Sharif
A. Sharif
May 30, 2015 · 4 min read

Introduction

Functional Programming in JavaScript has been the hype for a long time now and taking a functional approach when building complex UIs seems to be the way to go these days.

The rise of React as one of the major frameworks has opened the way for thinking about unidirectional data flows, components and immutability. At least React has done a better job in promoting those concepts to the masses.

Reacts power lies in creating immutable views that get re-rendered as soon the application state changes. Another strength is the virtual DOM, which detects any changes by diffing and allowing for minimal changes on the DOM (instead of having to re-render the complete DOM).

The introduction of immutable.js, mori and other immutable libraries has pushed the agenda even further. By combining React and immutable.js we can create efficient applications, with elegant and maintainable code and reduce complexity by avoiding having to maintain keeping state.


Immutability

Just think about a string in JavaScript.

var foo = 'Some String';var bar = foo.substr(1, 3);

Has foo changed? foo has not changed luckily. Strings and numbers are immutable in JavaScript, meaning that every operation on the string foo will return a new string not mutate it. This is what we would expect.

This works for strings and numbers obviously, but will not work when dealing with objects and arrays.

An object can change over time, so instead of mutating the existing reference, we want a new object returned as soon as we update the object. This is where immutable.js comes into play. We could also use another immutable library like mori for example, but for this example we will stick with the aforementioned.

We can summon that immutable data can not be changed. This leads to avoiding defensive copying and the support of an efficient change detection by using === operator to compare objects. This alone makes comparing objects very fast and efficient.

Building a Sequencer

Enough theory. We will build a simple Sequencer grid and add undo and redo options. This is what we expect visually:

A Grid consisting of sixteen rows displaying one row per instrument with every cell being clickable to toggle its active state.

Let us begin by defining a collection of rows and columns. This can easily be achieved by mapping on the instruments collection and returning 16 columns per instrument/row.

var instruments = [
‘kick’, ‘snare’, ‘hihat’, ‘open hihat’,
‘clap’, ‘shaker’, ‘noise’, ‘ad lib’,
‘piano’, ‘bass’, ‘random’, ‘loud snare’,
‘symbol’, ‘crash’, ‘other 1’, ‘other 2’
];
// build a 16 x 16 Grid
var grid = _.map(instruments, (title, index) => {
return {id: index, title: title, grids: _.map(_.range(16),
index => {
return {id: index, active: false};
})
}
});

Now that we have the basic grid ready, we can focus on the React specific parts.

var App = React.createClass({
getInitialState: function() {
return {
history: Immutable.List(),
future: Immutable.List(),
items: Immutable.fromJS(grid)
}
},

// other methods...

render: function() {
// to be implemented...
}
});
React.render(<App/>, window.appContainer);

So what is going on in that getInitialState method? We’re setting the initial application state by creating an immutable history and future list (for undo/redo) and also defining the original items by using the fromJS method. To quote the immutable.js documentation: fromJS() Deeply converts plain JS objects and arrays to Immutable Maps and Lists.

So we have an immutable collection of rows and grids now.

Implementing an undo/redo history is straightforward from there on:

undo: function() {
if (this.state.history.size < 1) return;
this.setState({
history: this.state.history.pop(),
future: this.state.future.push(this.state.items),
items: this.state.history.last()
});
},
redo: function() {
if (this.state.future.size < 1) return;
this.setState({
items: this.state.future.last(),
history: this.state.history.push(this.state.items),
future: this.state.future.pop()
});
},

As soon as an undo or redo is fired, we will check if there are any items in the history or future list and if so, update the current app state.

We also added an onClick method that will be passed on to child components and updates the items state as well as the history list as soon as it is called.

onClick: function(rowId, colId) {
var newItems =
this.state.items.updateIn([rowId, ‘grids’, colId], val => {
return val.set(‘active’, !val.get(‘active’));
});

this.setState({
history: this.state.history.push(this.state.items),
items: newItems
});
}

We should have covered the most important aspects in our base component. Here is the complete code for the root component including the render method:

Next we will take a look at the Sequencer component.

Eventually Sequencer only consists of a render method and delegates onClick on to the Row component.

To complete the example we’ll have a final look at the Row and Cell components too. These two only implement a render method as well. Nothing new going on in here.

Row and Cell only render the given data never operate on or change the data directly.

This all we needed to create a clickable grid with undo/redo functionality. The complete code and working example can be found here.

Now imagine trying to create the undo/redo functionality with mutable state. The power of this approach lies in structural sharing. If you’re interested in finding out more about how structural sharing works, about immutability or unidirectional data flow check the links below.

Special thanks to Stefan Oestreicher for helping out with the undo/redo functionality and other valuable input.


A. Sharif

Written by

A. Sharif

Focusing on quality. Software Development. Product Management.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade