Using Immutable.js with Redux

Contrary to my initial thinking, it turns out that using Immutable.js with Redux is not that complicated and is a good practice.

This article assumes that you are fairly familiar with the Redux library; if not, I wrote a series of articles on the topic starting with:

I later wrote an article describing why I didn’t use Immutable.js with Redux.

At the end of the article, however, I concluded that the situation was more complicated than I originally thought. In this article I continue to explore the topic through a couple of examples.

To build:

  1. Install Node.js.
  2. Download and extract the repository Redux Patterns (also used in the Redux By Example series).
  3. From the extracted folder, run npm install
  4. Run the command ./node_modules/.bin/babel src -d dist

Mutable

Before we introduce Immutable.js, let us remind ourselves of the problem of using Redux without it.

From the extracted folder, run the example with the command:

node dist/mutable.js

Looking at this example’s source code:

  • The state consists of two leaves, byId and ids; used to store an array of objects (items) with primitive properties.
  • The code is divided into sections: reducers, selectors, action creators, store, and exercising.

Let us walk through the exercising section side-by-side with the example’s output (the remaining sections are a fairly straightforward Redux implementation).

We first dispatch a FETCH action to initialize the state.

...
// EXERCISING - FETCH
store.dispatch(fetch(
[{
id: 'm',
name: 'mango',
description: 'Sweet and sticky',
}, {
id: 'n',
name: 'nectarine',
description: 'Crunchy goodness',
}],
));
...

At this point, the example’s output is (reflects an action occurred and that items has changed).

false

We then output the current value of the mango item.

...
// EXERCISING - OUTPUT CURRENT VALUE
state = store.getState();
let mango = getItem(state, 'm');
console.log('BEFORE UPDATE ACTION');
console.log(mango);
...

with expected result

...
BEFORE UPDATE ACTION
{ id: 'm', name: 'mango', description: 'Sweet and sticky' }

We then properly update the mango item.

...
// EXERCISING - UPDATE PROPER
mango.description = 'Sweet and super sticky'.
store.dispatch(update(mango));
...

As before, the example’s output is (reflects an action occurred and that items has changed).

...
false

We then output the current value of the mango item.

...
// EXERCISING - OUTPUT CURRENT VALUE
state = store.getState();
mango = getItem(state, 'm');
console.log('AFTER UPDATE ACTION');
console.log(mango);
...

with the expected result of

AFTER UPDATE ACTION
{ id: 'm', name: 'mango', description: 'Sweet and super sticky' }

We now improperly update the mango item (basically forgetting to dispatch the UPDATE action).

...
// EXERCISING - UPDATE IMPROPER
mango.description = 'Unripe and sour';
...

Unlike the proper update, the code that reflects an action occurred and that items has changed, did not run.

We then output the current value of the mango item.

...
// EXERCISING - OUTPUT CURRENT VALUE
state = store.getState();
mango = getItem(state, 'm');
console.log('AFTER IMPROPER UPDATE');
console.log(mango);
...

We can see, however, that the state indeed has changed.

...
AFTER IMPROPER UPDATE
{ id: 'm', name: 'mango', description: 'Unripe and sour' }

The crux of the problem is that we were able to directly manipulate the state without using actions, i.e., UPDATE. With this, the code that is listening for changes in the state never detects the change in items.

note: In the case of using Redux with React (and React-Redux) a connected component (to items) would not re-render; leaving the user-interface out-of-synch with the state.

Immutable

Let us refactor the last example with Immutable.js following the guidance provided by the Redux team.

We first install the Immutable.js library along with Redux-Immutable (provides a Immutable.js aware combineReducers function). From the command line in the example folder I ran:

npm install --save immutable
npm install --save redux-immutable

Then I updated (commented out old code and inserted new code) the example’s source code as follows:

First, I updated the imports.

/* eslint no-console: "off" */
// import { createStore, combineReducers } from 'redux';
import { createStore } from 'redux';
import { combineReducers } from 'redux-immutable';
import { normalize, schema } from 'normalizr';
import { createSelector } from 'reselect';
import { List, Map } from 'immutable';
...

Then I used the Immutable.js Map type to replace the JavaScript object for the byId leaf of the state.

...
const byId = ( /* state = {} */ state = Map({}), action) => {
switch (action.type) {
case 'FETCH':
case 'ADD':
case 'UPDATE':
/*
return {
...state,
...action.value.entities.items,
};
*/
return state.merge(action.value.entities.items);
case 'REMOVE':
/*
const newState = { ...state };
delete newState[action.value.result];
return newState;
*/
return state.delete(action.value.result);

default:
return state;
}
};
...

Likewise, I used the Immutable.js List type to replace the JavaScript array for the ids leaf of the state.

...
const ids = ( /* state = [] */ state = List([]), action) => {
switch (action.type) {
case 'FETCH':
// return action.value.result;
return List(action.value.result);

case 'ADD':
// return [...state, action.value.result];
return state.push(action.value.result);

case 'REMOVE':
/*
const newState = [...state];
newState.splice(state.indexOf(action.value.result), 1);
return newState;
*/
return state.delete(state.indexOf(action.value.result));

default:
return state;
}
};
...

As the entire state now consists of Immutable.js objects, we have to adapt the selectors with get methods. As per the guidance, the selectors return Immutable.js objects.

...
// SELECTORS
// const getItem = (state, id) => state.byId[id];
const getItem = (state, id) => state.get('byId').get(id);
// const getItemsIds = state => state.ids;
const getItemsIds = state => state.get('ids');

// const getItemsById = state => state.byId;
const getItemsById = state => state.get('byId');
const getItems = createSelector(
[getItemsIds, getItemsById],
// (itemsIds, itemsById) => itemsIds.map(id => itemsById[id]),
(itemsIds, itemsById) => itemsIds.map(id => itemsById.get(id)),

);
...

In this example, we will deviate from the guidance for simplicity; we will revisit in next example.

Using Immutable.JS everywhere keeps your code performant. Use it in your smart components, your selectors, your sagas or thunks, action creators, and especially your reducers.

— Redux Team

While we did use Immutable.js objects in the selectors and reducers, by leaving the action creators unchanged, their value properties are not Immutable.js objects.

From the extracted folder, run the example with the command:

node dist/immutable.js

As before, let us walk through the exercising section side-by-side with the example’s output.

As the action creators remain unchanged and the selectors have the same signature, the FETCH and OUTPUT CURRENT VALUE sections remain the same as the last example.

At this point, the example’s output is (reflects an action occurred and that items has changed).

false

Followed by the current value of the mango item (notice that it is now an Immutable.js Map object).

...
BEFORE UPDATE ACTION
Map { "id": "m", "name": "mango", "description": "Sweet and sticky" }

Because the mango item is an Immutable.js Map object, we need to use the set method to change the description. And because the action creators don’t use Immutable.js objects we need to use the dreaded to toJS method.

toJS() is an expensive function and negates the purpose of using Immutable.JS. Avoid its use.

— Redux Team

...
// EXERCISING - UPDATE PROPER
// mango.description = 'Sweet and super sticky';
mango = mango.set('description','Sweet and super sticky');
// store.dispatch(update(mango));
store.dispatch(update(mango.toJS()));
...

As before, the example’s output is (reflects an action occurred and that items has changed).

...
false

With the OUTPUT CURRENT VALUE section as before, we get the expected result of.

...
AFTER UPDATE ACTION
Map { "id": "m", "name": "mango", "description": "Sweet and super sticky" }

Because mango is an Immutable.js Map object, the set method without the UPDATE action…

...
// EXERCISING - UPDATE IMPROPER
// mango.description = 'Unripe and sour';
mango = mango.set('description','Unripe and sour');
...

…has no effect on the state (reflected in the OUTPUT CURRENT VALUE section output). This result differs from the last example and is the primary benefit of using the Immutable.js library with Redux.

...
AFTER IMPROPER UPDATE
Map { "id": "m", "name": "mango", "description": "Sweet and super sticky" }

Immutable-Fixed

By following the guidance and using Immutable.js in the action creators we can get rid of the expensive toJS function. One issue, however, is that the action creators in the previous examples rely on the normalizr library (unaware of Immutable.js).

Turns out that removing the normalizr library from the action creators only makes the reducers marginally more complicated.

Looking at the updated code; we got rid of the normalizr import and usage. We also will need the fromJS function from immutable.

...
// import { normalize, schema } from 'normalizr';
import { createSelector } from 'reselect';
// import { List, Map } from 'immutable';
import { fromJS, List, Map } from 'immutable';
/*
const itemSchema = new schema.Entity('items');
const itemsSchema = new schema.Array(itemSchema);
*/
...

Without the normalizr library and with action creators using Immutable.js, the byId reducer is somewhat more complex.

...
const byId = ( state = Map({}), action) => {
switch (action.type) {
case 'FETCH': {
const entry = {}; // INTERNALLY NOT USING IMMUTABLE.JS
for (let i = 0; i < action.value.size; i += 1) {
const item = action.value.get(i);
entry[item.get('id')] = item;
}
return state.merge(entry);
}
case 'ADD':
case 'UPDATE': {
// return state.merge(action.value.entities.items);
const entry = {};
entry[action.value.get('id')] = action.value;
return state.merge(entry);
}

case 'REMOVE':
// return state.delete(action.value.result);
return state.delete(action.value.get('id'));
default:
return state;
}
};
...

The ids reducer, however, is only slightly different.

...
const ids = (state = List([]), action) => {
switch (action.type) {
case 'FETCH':
// return List(action.value.result);
return action.value.map(o => o.get('id'));
case 'ADD':
// return state.push(action.value.result);
return state.push(action.value.get('id'));
case 'REMOVE':
// return state.delete(state.indexOf(action.value.result));
return state.delete(state.indexOf(action.value.get('id')));
default:
return state;
}
};
...

The selectors remain unchanged and the action creators are simplified:

...
// ACTION CREATORS
const fetch = items => ({
type: 'FETCH',
// value: normalize(items, itemsSchema),
value: items,
});
const update = item => ({
type: 'UPDATE',
// value: normalize(item, itemSchema),
value: item,
});
...

As the action creators now expect Immutable.js objects, we use fromJS to convert the array of objects to a List of Maps for fetch.

...
// store.dispatch(fetch(
store.dispatch(fetch(fromJS(
[{
id: 'm',
name: 'mango',
description: 'Sweet and sticky',
}, {
id: 'n',
name: 'nectarine',
description: 'Crunchy goodness',
}],
// ));
)));
...

As mango is already an Immutable.js Map, we can simply pass it directly to the update action creator; in doing so, getting rid of the expensive toJS use).

...
// store.dispatch(update(mango.toJS()));
store.dispatch(update(mango));
...

Conclusion

While I am still new to using Immutable.js, having written this article has:

  • One, confirmed that on my next React / Redux project I need to use it.
  • Two, provided me confidence that using it does not complicate things too much.
Show your support

Clapping shows how much you appreciated John Tucker’s story.