Using Immutable Data Structures

With React’s virtual DOM, immutable data structures make a lot of sense to make diffing more efficient, and immutable data structures have the additional benefit of preventing views from updating store data in a Flux architecture. For reasons like this, it makes a lot of sense to incorporate immutable data structures into your alt app, and where they come into play primarily is in your stores.

We will focus on Facebook’s Immutable library.

Alt <3s Immutable

Alt has first-class support for immutable data structures via the ImmutableUtil.

Getting started is simple, you’ll require the utility and pass your pre-wrapped stores to it.

var alt = new Alt();
var immutable = require('alt/utils/ImmutableUtil');

If you’re using babel with ES7 Stage 1 decorator support then this is sweet.

@immutable
class TodoStore {
  static displayName = 'TodoStore'

  constructor() {
    this.state = {
      todos: Immutable.Map({})
    };
  }
}

alt.createStore(TodoStore);

If you don’t wish to use ES7 decorators then no problem, they’re just sugar for function calls. You can just pass your store into the immutable function.

function TodoStore() {
  this.state = Immutable.Map({
    todos: Immutable.Map({})
  });
}
TodoStore.displayName = 'TodoStore';

alt.createStore(immutable(TodoStore));

A few things to note about immutable stores about this approach:

  • You use this.state to create your state rather than assigning directly to instance properties.
  • You specify your own Immutable data structure you wish to use. In this example we’re using Map.

Using your ImmutableStore is a bit different from using a regular store:

function TodoStore() {
  this.state = Immutable.Map({
    todos: Immutable.Map({})
  });

  this.bindListeners({
    addTodo: TodoActions.addTodo
  });
}

TodoStore.prototype.addTodo = function (todo) {
  var id = String(Math.random());
  this.setState(this.state.setIn(['todos', id], todo));
};

TodoStore.displayName = 'TodoStore';

var todoStore = alt.createStore(immutable(TodoStore));
  • You’ll be using setState in order to modify state within the store.
  • You can access the immutable object by using the accessor of this.state. In this example we’re using Map’s set method to set a new key and value.
todoStore.getState() // Immutable.Map

getState will return the Immutable object. This means if you’re using React you can use something like === in shouldComponentUpdate to get the performance benefits.

If you wish to convert your structure to a JS object/from a JS object you can use Immutable’s toJS() and fromJS() methods.

Last but not least, snapshots and bootstrapping just works when you’re using this util. The data structures are serialized and deserialized automatically.

Manually using ImmutableJS in your Stores

Record

One of the easiest ways to start getting some of the benefits of immutable is to take advantage of Immutable’s Record types. This method will result in the smallest changes for existing projects. You can read more about them on Facebook’s docs, but the best thing about Records is that they enable you to access values the same way you would from a normal JS object (object.prop). This means no changes to the view code using the immutable data and our changes only occur in store methods that return data to the view.

Here is an example of how a Record can be used:

// MyStore.js
import {Record} from 'immutable';

class MyStore {
  constructor() {
    this.data = {
      prop1: 1,
      prop2: 2
    };
  }

  getImmutState() {
    var ObjectRecord = Record(this.getState());
    return new ObjectRecord();
  }
}

// MyComponent.js
import {Component} from 'react';
import MyStore from 'stores/MyStore';

class MyComponent extends Component {
  render() {
    var storeData = MyStore.getImmutState();
    return (
      <div>Prop1: {storeData.prop1}</div>
    );
  }
}

fromJS

Immutable has a nice helper, fromJS that allows us to convert JS object/arrays to immutable Maps and Lists. This is an easy way to convert plain JS store data to immutable data structures before sending to the view. Unlike the “Record method” described above, you must remember to use getters to access data within these immutable objects.

Here is an example of using fromJS to return immutable data:

// MyStore.js
import Immutable from 'immutable';

class MyStore {
  constructor() {
    this.data = {
      prop1: 1,
      prop2: 2
    };
  }

  getImmutState() {
    return Immutable.fromJS(this.getState());
  }
}

// MyComponent.js
import {Component} from 'react';
import MyStore from 'stores/MyStore';

class MyComponent extends Component {
  render() {
    var storeData = MyStore.getImmutState().get('data');
    return (
      <div>Prop1: {storeData.get('prop1')}</div>
    );
  }
}

Using Immutable Data in Stores/Everywhere

You can also use Immutable’s data structures in your stores or throughout your app. You just need to remember that you need to use getters to access the data in your views or wherever you are reading it.

Immutable provides many nice data structures like Map, List, Set, etc.

Here is a basic example of using immutable data structures in your stores, rather than just returning immutable data structures to the view from them.

// MyStore.js
import {Map} from 'immutable';

class MyStore {
  constructor() {
    this.data = new Map({
      prop1: 1,
      prop2: 2
    });
  }

  onUpdateProp1(newProp1) {
    this.data = this.data.set('prop1', newProp1);
  }
}

// MyComponent.js
import {Component} from 'react';
import MyStore from 'stores/MyStore';

class MyComponent extends Component {
  render() {
    var storeData = MyStore.getState().get('data');
    return (
      <div>Prop1: {storeData.get('prop1')}</div>
    );
  }
}

Serializing/Deserializing Immutable Data

If you are using immutable data and plan on taking advantage of alt’s snapshot and bootstrap capabilities you must ensure that the onSerialize and onDeserialize hooks handle the immutable data.

Serialize returns the data from the store to be used in a snapshot so the immutable getters will need to be used to return a plain JS object to be serialized.

Deserialize takes bootstrap data and uses it to set the state of the store. This means in order for your store to function as you initially set it up, the bootstrapped data must be converted back to immutable data structures.

Example serialize/deserialize with immutable data structures:

// MyStore.js
import Immutable, {Map} from 'immutable';

class MyStore {
  constructor() {
    this.data = new Map({
      prop1: 1,
      prop2: 2
    });
  }

  static config = {
    onSerialize(state) {
      return {
        data: state.data.toJS()
      }
    },
    onDeserialize(data) {
      return Immutable.fromJS({
        prop1: data.prop1,
        prop2: data.prop2
      });
    }
  }
}