Immutable JS: worth the cost?

Introduction

I’m a front end developer with 4 years experience, currently working as part of a large team on a newly built React and Redux single page application.

Creating a greenfield website is an exciting prospect for all developers. Wide-eyed we grab a fist full of new technologies, throw them at a node server, stand back and marvel at our bleeding edge genius.

This post takes a look at one of our choices, ‘Immutable’ from Facebook. Set on developing in a functional programming paradigm we planned to use it to represent our data and enforce immutability.

Immutable data with Redux

Immutable data is a core concept in functional programming, and its use in JavaScript is in the ascendency. When working with React and Redux, immutable data helps reinforce one of their core principles: if the app state has not changed, neither should the DOM.

Many articles have been written about the benefit of using immutable data, some of the main benefits include:

  • Simplified data flow through apps.
  • Removed requirement for defensive copying of data.
  • Optimisation through data change detection.
  • Performance enhancement through memoization

‘Immutable’

‘Immutable’ is an open source library from Facebook which we incorporated into our app using redux-immutable. This allowed us to store our app state in the data types provided by ‘Immutable’.

To render the app’s state to the DOM we transferred it from the Redux store to the React components. This was achieved using the ‘connect()’ decorator function from react-redux.

Here follows some of the pros and cons we found during the development of our app.

[PRO] Enforced Immutability

The headline argument for using any library for immutable data types, is the reassurance that anyone working on your project cannot violate your principles of immutability.

As such ‘Immutable’ helps simplify development, as you never have to trace through your code to find where your data was altered. Instead your immutable data type will always represent precisely the app state present in the store.

The benefits of immutable data types outlined above can then be leveraged and everything seems peachy. However downsides do exist, downsides which may not present themselves until after development has begun in earnest.

[CON] Documentation & Debugging

Facebook have provided front end developers, not just a framework, but an entire ecosystem from which to build apps. However, when compared to the likes of React, the ‘Immutable’ documentation is painfully incomplete.

When a developer gets confused by the syntax of ‘Immutable’, or finds their code not working as expected, they turn to the documentation and regularly emerge none the wiser. Still without a clue why their code is not working as expected, they resort to the mighty ‘console.log()’. Unfortunately however, when inspecting their data, they may find themselves digging around in the props of a custom data type.

Console logged ‘Immutable’ object

This can be fixed by calling ‘toJS()’ on any ‘Immutable’ object to convert it back to a plain JavaScript object for logging. But small issues like these slow down development. Better documentation could improve this situation.

Regardless, requiring documentation and debugging to simply determine what data you have, is a bad foundation upon which to build.

[CON] Anti-Pattern Stink

Retrieving the data from the app store via the ‘connect()’ decorator gave us access to the ‘Immutable’ data objects, but we would commonly write components using native data types. To convert, we developed a pattern of using ‘toJS()’ in the ‘connect()’ decorator as follows:

// Get the data from the store
@connect((state) => {
// Convert store data to native Javascript objects
user: state.get(‘user’).toJS(),
wine: state.getIn([‘drinks’, ‘wines’]).toJS()
})
class HelloWine extends Component {
render() {
// ES6 de-structuring of data from props
const { user, wines: { houseRed } } = this.props
return <div>{`Hi ${ user }! Fancy some ${ houseRed }?`}</div>
}
}

This pattern seems convenient and harmless but when using mobile devices, we found that firing Redux actions was excruciatingly slow. Below is a JavaScript performance profile recorded whilst opening the side-menu of the app on a Samsung S5.

Profile of inefficient rendering

Over 2 seconds to open the menu. Not exactly snappy for a cutting edge web-app!

We traced the fault back to the above pattern. What was happening, under the hood, is that Redux dispatches the action object, updating the store with the new state produced by the ‘reducer()’ function.

Each ‘connect()’ decorated component checks if it’s data has updated, and if it has it will trigger a re-render through the React lifecycle. This allows Redux to selectively render React components, improving performance.

When running the ‘connect()’ function the state of the app is converted to a vanilla JavaScript object via ‘toJS()’. This creates a new object each time. So when compared to the previous state, even if it is the same ‘Immutable’ object, the object created is different. Meaning, upon on any action being fired, every ‘connect()’ decorated element and all of it’s children will be re-rendered.

If you take nothing else away from this, NEVER use toJS() inside the ‘connect()’ decorator function.

[CON] Not quite ES6

We decided that if our app state is stored in ‘Immutable’ data types then our components should utilise the same types. And so we refactored. The big downside being that you lose native functionality.

For example ES6 de-structuring becomes a combination of ‘get()’ and ‘getIn()’ function calls.

const { wines: { houseRed: { name, year } } } = this.props
// becomes
const { wines } = this.props
const name = wines.getIn([‘houseRed’, ‘name’])
const year = wines.getIn([‘houseRed’, ‘year’])

This is longer, less elegant, and I personally also dislike the prolific use of strings. A typo means you will simply receive ‘undefined’ and may miss the issue, rather than throw a JavaScript error alerting you to your mistake.

You also lose the ability to use ES6 spreading, which can make re-assigning props on components tedious.

<AmazingComponent { …props } />
<AmazingComponent prop1={ props.prop1 } prop2={ props.prop2 } prop3={ props.prop3 } />

The pain points of the ‘Immutable’ syntax reminded us why we developed the pattern of converting in the first place. Having issues handling your core data types, even if they are small gripes, can be very frustrating, and further waste valuable development time.

Outcome

After refactoring some components to use ‘Immutable’ we reevaluated our position. We discussed all the points above and concluded that the only benefit of ‘Immutable’ was a means of enforcing immutability, but what is the point? Truly Functional programming means you never attempt to mutate state, so it doesn’t matter if the state is technically mutable or not.

Considering all of the drawbacks we had run into whilst using ‘Immutable’, we decided to completely remove it from the project. Adhering to the principles of functional programming allows us to remain confident in our data. Confidence in our developers, combined with peer code review, is enough to ensure there are no silly mistakes.

Thanks for reading, hope it helps someone!