React, Redux, and Immutable.js: Ingredients for Efficient Web Applications
Unlike most front-end web frameworks, React’s aim is to solve the various challenges of building user interfaces that rely on changing data. Although React is a simple JavaScript library and is easy to get started with, it is still possible to misuse it in ways that deny the web app from reaping the benefits that React has to offer.
In this article, Toptal engineer Ivan Rogic demonstrates the synergy of React, Redux and Immutable.js, and shows how these libraries together can solve many performance issues that are often encountered in large web applications.
Unlike most front-end web frameworks, React’s aim is to solve the various challenges of building user interfaces that rely on changing data. Although React is a simple JavaScript library and is easy to get started with, it is still possible to misuse it in ways that deny the web app from reaping the benefits that React has to offer.
In this article, Toptal engineer Ivan Rogic demonstrates the synergy of React, Redux and Immutable.js, and shows how these libraries together can solve many performance issues that are often encountered in large web applications.
Ivan first started coding back in 2007 at the beginning of his college education, and he became really passionate about it.
Expertise
React, Redux and Immutable.js are currently among the most popular JavaScript libraries and are rapidly becoming developers’ first choice when it comes to front-end development. In the few React and Redux projects that I have worked on, I realised that a lot of developers getting started with React do not fully understand React and how to write efficient code to utilise its full potential.
In this Immutable.js tutorial, we will build a simple app using React and Redux, and identify some of the most common misuses of React and ways to avoid them.
Data Reference Problem
React is all about performance. It was built from the ground up to be extremely performant, only re-rendering minimal parts of DOM to satisfy new data changes. Any React app should mostly consist of small simple (or stateless function) components. They are simple to reason about and most of them can have shouldComponentUpdate function returning false.
shouldComponentUpdate(nextProps, nextState) {
return false;
}
Performance wise, the most important component lifecycle function is shouldComponentUpdate and if possible it should always return false. This ensures that this component will never re-render (except the initial render) effectively making the React app feel extremely fast.
When that is not the case, our goal is to make a cheap equality check of old props/state vs new props/state and skip re-rendering if the data is unchanged.
Let’s take a step back for a second and review how JavaScript performs equality checks for different data types.
Equality check for primitive data types like boolean, string and integer is very simple since they are always compared by their actual value:
1 === 1
’string’ === ’string’
true === true
On the other hand, equality check for complex types like objects, arrays and functions is completely different. Two objects are the same if they have the same reference (pointing to the same object in memory).
const obj1 = { prop: ’someValue’ };
const obj2 = { prop: ’someValue’ };
console.log(obj1 === obj2); // false
Even though obj1 and obj2 appear to be the same, their reference is different. Since they are different, comparing them naively within the shouldComponentUpdate function will cause our component re-render needlessly.
The important thing to note is that the data coming from Redux reducers, if not set up correctly, will always be served with different reference which will cause component to re-render every time.
This is a core problem in our quest to avoid component re-rendering.
Handling References
Let’s take an example in which we have deeply nested objects and we want to compare it to its previous version. We could recursively loop through nested object props and compare each one, but obviously that would be extremely expensive and is out of the question.
That leaves us with only one solution, and that is to check the reference, but new problems emerge quickly:
- Preserving the reference if nothing has changed
- Changing reference if any of the nested object/array prop values changed
This is not an easy task if we want to do it in a nice, clean, and performance optimised way. Facebook realised this problem a long time ago and called Immutable.js to the rescue.
import { Map } from ‘immutable’;
// transform object into immutable map
let obj1 = Map({ prop: ’someValue’ });
const obj2 = obj1;
console.log(obj1 === obj2); // true
obj1 = obj1.set(‘prop’, ’someValue’); // set same old value
console.log(obj1 === obj2); // true | does not break reference because nothing has changed
obj1 = obj1.set(‘prop’, ’someNewValue’); // set new value
console.log(obj1 === obj2); // false | breaks reference
None of the Immutable.js functions perform direct mutation on the given data. Instead, data is cloned internally, mutated and if there were any changes new reference is returned. Otherwise it returns the initial reference. New reference must be set explicitly, like obj1 = obj1.set(...);
.
React, Redux and Immutable.js Examples
The best way to demonstrate the power of these libraries is to build a simple app. And what can be simpler than a todo app?
For brevity, in this article, we will only walk through the parts of the app that are critical to these concepts. The entire source code of the app code can be found on GitHub.
When the app is started you will notice that calls to console.log are conveniently placed in key areas to clearly show the amount of DOM re-render, which is minimal.
Like any other todo app, we want to show a list of todo items. When the user clicks on a todo item we will mark it as completed. Also we need a small input field on top to add new todos and on the bottom 3 filters which will allow the user to toggle between:
- All
- Completed
- Active
Redux Reducer
All data in Redux application lives inside a single store object and we can look at the reducers as just a convenient way of splitting the store into smaller pieces that are easier to reason about. Since reducer is also a function, it too can be split into even smaller parts.
Our reducer will consist of 2 small parts:
- todoList
- activeFilter
// reducers/todos.js
import * as types from 'constants/ActionTypes';
// we can look at List/Map as immutable representation of JS Array/Object
import { List, Map } from 'immutable';
import { combineReducers } from 'redux';
function todoList(state = List(), action) { // default state is empty List()
switch (action.type) {
case types.ADD_TODO:
return state.push(Map({ // Every switch/case must always return either immutable
id: action.id, // or primitive (like in activeFilter) state data
text: action.text, // We let Immutable decide if data has changed or not
isCompleted: false,
}));
// other cases...
default:
return state;
}
}
function activeFilter(state = 'all', action) {
switch (action.type) {
case types.CHANGE_FILTER:
return action.filter; // This is primitive data so there’s no need to worry
default:
return state;
}
}
// combineReducers combines reducers into a single object
// it lets us create any number or combination of reducers to fit our case
export default combineReducers({
activeFilter,
todoList,
});
Connecting with Redux
Now that we have set up a Redux reducer with Immutable.js data, let’s connect it with React component to pass the data in.
// components/App.js
import { connect } from 'react-redux';
// ….component code
const mapStateToProps = state => ({
activeFilter: state.todos.activeFilter,
todoList: state.todos.todoList,
});
export default connect(mapStateToProps)(App);
In a perfect world, connect should be performed only on top level route components, extracting the data in mapStateToProps and the rest is basic React passing props to children. On large scale applications it tends to get hard to keep track of all the connections so we want to keep them to a minimum.
It is very important to note that state.todos is a regular JavaScript object returned from Redux combineReducers function (todos being the name of the reducer), but state.todos.todoList is an Immutable List and it is critical that it stays in such a form until it passes shouldComponentUpdate check.
Avoiding Component Re-render
Before we dig deeper, it is important to understand what type of data must be served to the component:
- Primitive types of any kind
- Object/array only in immutable form
Having these types of data allows us to shallowly compare the props that come into React components.
Next example shows how to diff the props in the simplest way possible:
$ npm install react-pure-render
import shallowEqual from 'react-pure-render/shallowEqual';
shouldComponentUpdate(nextProps, nextState) {
return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);
}
Function shallowEqual will check the props/state diff only 1 level deep. It works extremely fast and is in perfect synergy with our immutable data. Having to write this shouldComponentUpdate in every component would be very inconvenient, but fortunately there is a simple solution.
Extract shouldComponentUpdate into a special separate component:
// components/PureComponent.js
import React from 'react';
import shallowEqual from 'react-pure-render/shallowEqual';
export default class PureComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);
}
}
Then just extend any component in which this shouldComponentUpdate logic is desired:
// components/Todo.js
export default class Todo extends PureComponent {
// Component code
}
This is a very clean and efficient way of avoiding component re-render in most cases, and later if the app gets more complex and suddenly requires custom solution it can be changed easily.
There is a slight problem when using PureComponent while passing functions as props. Since React, with ES6 class, does not automatically bind this to functions we have to do it manually. We can achieve this by doing one of the following:
- use ES6 arrow function binding:
<Component onClick={() => this.handleClick()} />
- use bind:
<Component onClick={this.handleClick.bind(this)} />
Both approaches will cause Component to re-render because different reference has been passed to onClick every time.
To work around this problem we can pre-bind functions in constructor method like so:
constructor() {
super();
this.handleClick = this.handleClick.bind(this);
}
// Then simply pass the function
render() {
return <Component onClick={this.handleClick} />
}
If you find yourself pre-binding multiple functions most of the time, we can export and reuse small helper function:
// utils/bind-functions.js
export default function bindFunctions(functions) {
functions.forEach(f => this[f] = this[f].bind(this));
}
// some component
constructor() {
super();
bindFunctions.call(this, ['handleClick']); // Second argument is array of function names
}
If none of the solutions work for you, you can always write shouldComponentUpdate conditions manually.
Handling Immutable Data Inside a Component
With the current immutable data setup, re-render has been avoided and we are left with immutable data inside a component’s props. There are number of ways to use this immutable data, but the most common mistake is to convert the data right away into plain JS using immutable toJS function.
Using toJS to deeply convert immutable data into plain JS negates the whole purpose of avoiding re-rendering because as expected, it is very slow and as such should be avoided. So how do we handle immutable data?
It needs to be used as is, that’s why Immutable API provides a wide variety of functions, map and get being most commonly used inside React component. todoList data structure coming from the Redux Reducer is an array of objects in immutable form, each object representing a single todo item:
[{
id: 1,
text: 'todo1',
isCompleted: false,
}, {
id: 2,
text: 'todo2',
isCompleted: false,
}]
Immutable.js API is very similar to regular JavaScript, so we would use todoList like any other array of objects. Map function proves best in most cases.
Inside a map callback we get todo, which is an object still in immutable form and we can safely pass it in Todo component.
// components/TodoList.js
render() {
return (
// ….
{todoList.map(todo => {
return (
<Todo key={todo.get('id')}
todo={todo}/>
);
})}
// ….
);
}
If you plan on performing multiple chained iterations over immutable data like:
myMap.filter(somePred).sort(someComp)
… then it is very important to first convert it into Seq using toSeq and after iterations turn it back to desired form like:
myMap.toSeq().filter(somePred).sort(someComp).toOrderedMap()
Since Immutable.js never directly mutates given data, it always needs to make another copy of it, performing multiple iterations like this can be very expensive. Seq is lazy immutable sequence of data, meaning it will perform as few operations as possible to do its task while skipping creation of intermediate copies. Seq was built to be used this way.
Inside Todo component use get or getIn to get the props.
Simple enough right?
Well, what I realized is that a lot of the time it can get very unreadable having a large number of get()
and especially getIn()
. So I decided to find a sweet-spot between performance and readability and after some simple experiments I found out that Immutable.js toObject and toArray functions work very well.
These functions shallowly convert (1 level deep) Immutable.js objects/arrays into plain JavaScript objects/arrays. If we have any data deeply nested inside, they will remain in immutable form ready to be passed to
It is slower than get()
just by a negligible margin, but looks a lot cleaner:
// components/Todo.js
render() {
const { id, text, isCompleted } = this.props.todo.toObject();
// …..
}
Let’s See It All in Action
In case you have not cloned the code from GitHub yet, now is a great time to do it:
git clone https://github.com/rogic89/ToDo-react-redux-immutable.git
cd ToDo-react-redux-immutable
Starting the server is as simple (make sure Node.js and NPM are installed) as this:
npm install
npm start
Navigate to http://localhost:3000 in your web browser. With the developer console open, watch the logs as you add a few todo items, mark them as done and change the filter:
- Add 5 todo items
- Change filter from ‘All’ to ‘Active’ and then back to ‘All’
- No todo re-render, just filter change
- Mark 2 todo items as completed
- Two todos were re-rendered, but only one at a time
- Change filter from ‘All’ to ‘Active’ and then back to ‘All’
- Only 2 completed todo items were mounted/unmounted
- Active ones were not re-rendered
- Delete a single todo item from the middle of the list
- Only the todo item removed was affected, others were not re-rendered
Wrap Up
The synergy of React, Redux and Immutable.js, when used right, offer some elegant solutions to many performance issues that are often encountered in large web applications.
Immutable.js allows us to detect changes in JavaScript objects/arrays without resorting to the inefficiencies of deep equality checks, which in turn allows React to avoid expensive re-render operations when they are not required. This means Immutable.js performance tends to be good in most scenarios.
I hope you liked the article and find it useful for building React innovative solutions in your future projects.
Further Reading on the Toptal Blog:
- Immutability in JavaScript Using Redux
- Working With the React Context API
- The Best React State Management Tools for Enterprise Applications
- Efficient React Components: A Guide to Optimizing React Performance
- Angular vs. React: Which Is Better for Web Development?
- Next.js vs. React: A Comparative Tutorial
Understanding the basics
What is Redux?
Redux is an open-source JavaScript predictable state container. Redux is typically used to create user interfaces, in conjunction with libraries such as React or Angular.
What is Immutable.js?
Immutable.js is a library designed for the creation of immutable collections of data. It is commonly used in React/Redux development. Immutable.js was created by Facebook.
What does immutable mean?
Objects that can’t be modified after creation are considered immutable objects, as their state and properties cannot be modified once they are instantiated. This is in contrast to mutable objects which, as their name suggests, can be changed.
Osijek, Croatia
Member since September 30, 2015
About the author
Ivan first started coding back in 2007 at the beginning of his college education, and he became really passionate about it.