Practical Guide to Using ImmutableJS with Redux and React

Ian Carlson
7 min readMay 29, 2017

--

In a previous blog, I went into the pros and cons of using ImmutableJS with React and Redux. Now, I’ll attempt to go through a basic example of using everything together and go into more advanced topics further down. Even if you have an existing codebase, you can start adding immutable parts of your state tree one piece at a time.

Now let’s start with a simple immutable reducer. Create a record that establishes the model, or shape, of the reducer. The big advantage to records is they allow using the dot notation for reading values and they don’t allow other properties to be added after defining the record shape.

import { Record, List } from 'immutable';// First create a record definition
const myRecord = Record({
loading: true,
aNumber: 0,
aList: List(),
});
// Now create a record instance
const initialState = myRecord({
loading: true,
aNumber: 0,
aList: List(),
});

The first step is create the record definition. The values set in the definition establish the defaults. The second part creates the record instance. The values set here override the defaults. If one of the properties was not initialized on instance creation then it would take the default value.

Now, let’s stub out the reducer:

// file myReducerAndActions.jsimport { Record, List } from 'immutable';
import { handleActions } from 'redux-actions';
const myRecord = Record({
loading: true,
aNumber: 0,
aList: List(),
});
// Since the defaults are the same as the set values, we could
// just not pass in an object and get the same result
// i.e., const initialState = myRecord();
const initialState = myRecord({
loading: true,
aNumber: 0,
aList: List(),
});
const actions = {}; // to be filled in laterexport default handleActions(actions, initialState);

You might be confused by the use of capitalizing the first letter without putting new into front of it. ImmutableJS has their own class creation code that makes the new optional. Not sure why they decided to allow both methodologies, but the only way I know how to get Flow record annotations working is doing it this way.

The main Redux store should probably go in its own file. The process of registering an immutable reducer is the same as a regular one, but I’ll provide it here for context:

// file mainStore.jsimport {
createStore,
combineReducers,
applyMiddleware,
} from 'redux';
import myReducer from './myReducerAndActions.js';
import createLogger from 'redux-logger';
import _mapValues from 'lodash/mapValues';
import thunk from 'redux-thunk';
let middlewares = [thunk];

if (process.env.NODE_ENV !== 'production'
&& process.env.TEST !== 'true'
) {
const logger = createLogger({
collapsed: true,
stateTransformer: state =>
// Not all the reducers are Immutable structures so have
// to check if they are for each one.
// This function could get very costly over time, but it's
// super useful for debugging.
_mapValues(state, (reducer) => {
if (reducer.toJS) {
return reducer.toJS();
}

return reducer;
}),
});

middlewares = [...middlewares, logger];
}

const rootReducer = combineReducers({
myReducer,
});

const store = createStore(
rootReducer,
applyMiddleware(...middlewares),
);

export default store;

I included the logger middleware code because there is some logic that calls toJS() on an immutable reducer to make the state transitions readable from the console. This can get a little bogged down because of how expensive toJS() is, but so far it hasn’t been too bad. The code doesn’t run during automated testing or production builds so it’s really just for debugging during development. Using the Lodash mapValues() function makes the code cleaner, but this can be done however you like.

Ok, so now we have most of the code in place let’s take a look at adding functions to our reducer to handle different types of actions.

// file myReducerAndActions.jsimport { Record, List } from 'immutable';
import { handleActions, createAction } from 'redux-actions';
const FETCH_DATA_REQUEST = 'FETCH_DATA_REQUEST';
const fetchDataRequest = createAction(FETCH_DATA_REQUEST);
const FETCH_DATA_RESPONSE = 'FETCH_DATA_RESPONSE';
const fetchDataResponse = createAction(FETCH_DATA_RESPONSE);
const fetchData = () =>
(dispatch) => {
dispatch(fetchDataRequest());
fetch('/awesomeEndpoint', { // fetch is global
method: 'GET',
})
.then((response) => {
if (!response.ok) {
return new Error(response);
}
return response.json();
})
.then((data) => {
dispatch(fetchDataResponse(data));
})
.catch((error) => {
dispatch(fetchDataResponse(error));
});
};
const myRecord = new Record({
loading: true,
aNumber: 0,
aList: List(),
});
const initialState = myRecord({
loading: true,
aNumber: 0,
aList: List()
});
const actions = {
[FETCH_DATA_REQUEST]: state => state.set('loading', true),
[FETCH_DATA_RESPONSE]: (state, action) => {
if(action.payload instanceof Error) {
// do something useful here
return state.set('loading', false);
}
return state
.set('loading', false)
// let's pretend the payload is an array of strings
.set('aList', state.aList.merge(action.payload)),
};
export { fetchData };export default handleActions(actions, initialState);

And here’s the React component to display the data:

// file SimpleComponentContainer.jsx
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchData } from './myReducerAndActions.js';class SimpleComponentContainer extends Component {
componentDidMount() {
this.props.dispatch(fetchData);
}
render() {
const { loading, aNumber, aList } = this.props.reducer;
return (
<div>
{loading
? <div>show spinning loader</div>
: <div>
<div>show loaded content</div>
<div>{aNumber}</div>
<div>
{aList.map(item => <div>item</div>)}
</div>
</div>}
</div>
);
}
}
// Map all the reducer properties into this component
export default connect(state => ({
reducer: state.myReducer,
}))(SimpleComponentContainer);

Somewhere this component needs to be a child of the top-level app component, but I’m not including it for simplicity. So just for clarity, we have three files now:

- mainStore.js
- myReducerAndActions.js
- SimpleComponentContainer.jsx

In SimpleComponentContainer.jsx, the component is bound to myReducer via the connect() call at the bottom. When the component mounts it will dispatch the fetchData action — which is actually a thunk, but just think of it as a action that has bundled sync and async actions. After the fetchData action is dispatched, the render function gets called twice: once for setting the loading flag, and once for displaying the loaded data.

Notice that because we used an immutable record, accessing the state properties is simple and relatively transparent using the dot notation instead of get(). We didn’t need to use toJS().

ImmutableJS supports collections that contain immutable and mutable objects. Why is this important? Because the top-level of the collection may be immutable, but nested objects could be regular objects that can be mutated at will and won’t give you the benefits of immutability. Not only that, it adds unnecessary mental context switching when reading and debugging code. Here’s an example:

const collection = Map({
nestedObject: {
nestedKey: 0,
},
nestedArray: [],
});
console.log(collection.get('nestedObject').nestedKey); // 0
// If collection were immutable all the way to the lowest level
// the code would be
// console.log(collection.getIn(['nestedObject', 'nestedKey']));

In this case, nestedKey is actually contained within a mutable object. One way to deeply convert to immutable is using fromJS():

const immutableCollection = fromJS({
nestedObject: {
nestedKey: 0,
},
nestedArray: [],
});

I strongly recommend all data collections be completely immutable, top to bottom, if you can swing it. If the project is completely new and there’s no pre-existing code this approach should be straight-forward. However, with a bunch of React components already in the codebase that don’t take immutable collections, it may not be practical to refactor them all at once. That’s when a partially immutable Redux state tree makes sense. Some people have addressed this problem by calling toJS() in the connect() function so they don’t have to refactor any of the React components. While this being very easy, it comes at a huge cost in performance in production code and will make you hate using Immutable. Since the render function gets called many times throughout the life of a component, it needs to be as lean as possible. Calling toJS() in the render function ,or in connect(), makes the code do the recursive conversion every time. Don’t do it.

In some edge cases, you may need to preserve regular javascript objects without converting them to immutable, but this is should be the exception not the rule. An example of this is passing React classes or elements through the reducer or though components as props. ImmutableJS will mangle these types of things — if you do recursive conversion with fromJS() or merge() — and you’ll end up having to call toJS() to unmangle it.

Since the ImmutableJS API closely mimics the ES6 standard, a lot of the built-in methods are compatible with both. Things like map(), reduce(), filter() use the same syntax. So refactoring isn’t so bad if you’re already doing it this way. The most common difference being arrays have the length property and immutable lists have the size property. Even if you use Lodash quite a bit, which I think most of us do, then refactor is still pretty straight-forward, just a little more labor intensive — the syntax is more different, but the concepts are the same.

The beauty of ImmutableJS is you can start out pretty basic, like in the examples above, and really grow into it as feels more comfortable. It has a lot more computer sciency data structures like Sets and OrderedMaps available. It also provides easy ways to optimize the performance of your code. For example, you can write custom shouldComponentUpdate() methods in React components to do cheap comparisons between old and new data to see if rendering is necessary. Because Immutable updates object references effectively, you can use object reference/identity comparisons via === to detect change. I also go into all the other benefits here as well.

--

--