Immutable.js, the 80/20 Rule for React and Redux

I've scoured the internet for some basic information on Immutable.js to help with the performance on some of my React applications. But most only cover some of the basics or examples that don't use data structures that are common in most API.

Luckily for most coming from a functional programming background, (in JS, those familiar with lodash or underscore) the conceptual mental models and DSL's are relatively the same in Immutable.js.

First we have to understand the two main data structures: Map and List

Map
const map = Immutable.Map({ id: 1, name: 'John' });  
map.get('name') == 'John'; // true  
map.set('name', 'Mike');  
map.get('name') == 'Mike'; // true  
List
const list = Immutable.List([5, 20, 101];  
list.get(1) == 20; // true  
list.set(0, 9);  
list.get(0) == 9; // true  
list.set(4, 30);  
list.toJS(); // [9, 20, 101, undefined, 30]  

Converting to Immutable.js, fromJS magically detects the proper data structure to convert to. In this example, Immutable.js knows to create a List of Map from an Array of Objects.

const data = [ { id: 1, name: "John" }, { id: 2, name: "Adam" } ];  
const magic = Immutable.fromJS(data);

Immutable.List.isList(magic); // true  
Immutable.Map.isMap(magic.get(0)); // true

Of course, this can done manually as shown below

const result = [ { id: 1, name: "John" }, { id: 2, name: "Adam" } ];

const magic = Immutable.fromJS(result);

let list = Immutable.List();  
result.map((item, index) => { list = list.set(index, Immutable.Map(item))});  
Immutable.is(list, magic); // true,  

Okay, with these basic data structure building blocks, lets build a basic redux store using Immutable.js

Let's take a look at this Person reducer using List and Map

const initialState = Immutable.List([  
  Immutable.Map({ id: 1, name: "John Snow", title: "King of the North" }), 
  Immutable.Map({ id: 2, name: "Adam", title: "Butcher" })
]);
function personReducer(state = initialState, action) {  
  switch (action.type) {
    case FETCH_ALL_PEOPLE:  // O(n)
      action.payload.map((item, index) => { 
        state = state.set(index, Immutable.Map(item));
      });
      // OR state = Immutable.fromJS(action.payload);
      return state;
    case FETCH_ALL_KINGS: // O(n^2)
      action.payload.map((king, index) => {
        const index = state.findIndex((item) => {
          return item.get('id') === king.id;
        });
        if ( index >= 0 ) {
          state = state.set(index, Immutable.Map(king));
        } else {
          state.push(Immutable.Map(king));
        }
      });
      return state;
    case CREATE_PERSON: // O(1)
      return state.push(Immutable.Map(action.payload));
    case GET_PERSON: // O(n)
      const index = state.findIndex((item) => {
        return item.get('id') === action.payload.id;
      });
      if ( index >= 0 ) {
        return state.update(index, (person) => {
          return person.set('name', action.payload.name);
        });
      } else {
        state.push(Immutable.Map(action.payload));
      }
      return state;
    case DELETE_PERSON: // O(n)
      const index = state.findIndex((item) => {
        return item.get('id') === action.payload.id;
      });
      return state.delete(index);
    case UPDATE_PERSON: // O(n)
      const index = state.findIndex((item) => {
        return item.get('id') === action.payload.id;
      });
      if ( index >= 0 ) {
        state = state.set(index, Immutable.Map(action.payload));
      }
      return state;
    default:
      return state;
  }
}

Things are okay here except when we FETCH_ALL_KINGS and get O(n^2) by trying to combine an Array with a List. We also have the following sprinkled in the code to check for duplicates which makes those cases a minimum of O(n) complexity

const index = state.findIndex((item) => {  
  return item.get('id') === action.payload.id;
});
Set

Set duplicate checks are done very efficiently. Lets rewrite the above code using Set instead of List

const initialState = Immutable.Set([  
  Immutable.Map({ id: 1, name: "John Snow", title: "King of the North" }), 
  Immutable.Map({ id: 2, name: "Adam", title: "Butcher" })
]);
function personReducer(state = initialState, action) {  
  switch (action.type) {
    case FETCH_ALL_PEOPLE:  // O(nlogn)
    case FETCH_ALL_KINGS:  // O(nlogn)
      action.payload.map((item, index) => { 
        state = state.add(Immutable.Map(item));
      });
      return state;
    case CREATE_PERSON: // O(logn)
    case GET_PERSON: // O(logn)
      return state.add(Immutable.Map(action.payload));
    case DELETE_PERSON: // O(n)
      return state.delete(Immutable.Map(action.payload));
    case UPDATE_PERSON: // O(n)
      // This is a combo of DELETE_PERSON then CREATE_PERSON
      const found = state.find((person) => {
        return action.payload.id == person.get('id');
      });
      if (found) {
       state = state.delete(Immutable.Map(found));
       state = state.add(Immutable.Map(action.payload));
      }
      return state;
    default:
      return state;
  }
}

The code state.add(Immutable.Map(action.payload)); checks for duplicates and if it does't exist, it adds it to the Set. Similarly, state.delete(Immutable.Map(action.payload)) is able to find the same data objects and delete it properly.

With this, you'll notice that the worse efficiency is at O(nlogn) which is a great trade off vs O(n^2). The code is also more clean and readable. A drawback of Set is that it isn't ordered like List so if order isn't important, the tradeoffs are worth it.

Record

If you noticed with Map person.get('name') isn't nearly as elegant as person.name in vanilla javascript on top of also losing a lot of the object syntactic sugar ES6+ provides. Luckily Immutable.js provides the data structure Record, which is essentially Map but can be treated like a javascript object

const Person = Immutable.Record({ id: "", name: "" });  
const person = new Person({ id: 1, name: "John Snow" });

person.name // "John Snow"  
const { name } = person;  // name = "John Snow"  

We just covered the basics of Map, List, Set and Record. These data structures is just the tip of the iceberg but should be a great starting point for Immutable.js

Comments powered by Disqus