We’re using Redux on a project at work and a couple of months ago we decided it was time to ditch Object.assign and the spread operator in favor of an actual immutability solution for Redux.

We used the following criteria to inform our decision:

  • The library should leave a small syntactic footprint. We didn’t want to pollute our code base with a noticeable amount of extra syntax and boilerplate.
  • Immutability should be enforced. Redux fundamentally depends on the redux stores being truly immutable.
  • Persistent data structures was a nice-to-have. The size of our data and the frequency that our data changes didn’t concern us to the point of seeking out an immutability solution which leveraged persistent data structures.

We found that seamless-immutable better aligned with what we were looking for in an immutability solution than ImmutableJS.

Brevity of Syntax

This is where seamless-immutable really lives up to its name. When using a seamless immutable object or array I can use it as if it was a regular Javascript object or array, with the one exception that I can’t mutate it.

const myArr = Immutable([
    {val: 'hello', count: 1},
    {val: 'world', count: 2}
]);
myArr
    .map(item => item.val)
    .forEach(item => {
        console.log(item);
    });
// > hello
// > world
myArr.reduce((acc, curr) => acc + curr.count, 0); // 3
const otherArray = Immutable([1, 2]).concat([3, 4]); // [1, 2, 3, 4]

The brevity of seamless-immutable’s syntax is also really nice. This is best explained by example and the following example includes most of how we’re using this library.

const DEFAULT_STATE = Immutable({
    users: [],
    items: [],
    error: null
});
const nextState = DEFAULT_STATE.merge({
    users: ['Sally', 'John'],
    items: [{ name: 'item1' }]
}, {deep: true});
/* Or equivalently
const nextState = DEFAULT_STATE
                     .set('users', ['Sally', 'John'])
                     .set('items', [{ name: 'item1' }]);
*/
// read on about the need for mutable data
let mutableItems = nextState.items.asMutable();

Immutable(), set(), and merge() consist of the majority of “extra syntax” that we wouldn’t otherwise have, with the occasional asMutable() sprinkled in because.. Angular (see below). Just to reiterate, every one of those operations returns an object or array that looks and quacks like an object or array respectively, with the majority of object/array methods available too.

Enforced Immutability

Attempting to mutate a seamless-immutable object will throw a runtime error.

const myObj = Immutable({
    user: {
        name: 'Charlie',
        colors: ['red', 'blue']
    }
});
myObj.user.name;
// "Charlie"
myObj.user.name = 'Chuck';
// Uncaught TypeError: Cannot assign to read only property 'name' of object

While a runtime exception isn’t the perfect answer to enforcing immutability, it at least gives us the guarantee that our immutable data will never change.

We found that we were able to sufficiently circumvent these runtime exceptions by writing unit tests for both our reducers and our selectors by using mock data that was wrapped in a call to Immutable(). If the functions we tested threw a runtime exception we knew those function were attempting to mutate immutable data. That extra call in our unit tests was a negligible price to pay to ensure we don’t receive those runtime exceptions.

A quick caveat..

We’re using AngularJS with Redux as a state management solution. AngularJS is very opinionated and oftentimes oversteps its bounds. This is one of those times.

When you pass an array to ng-repeat, Angular will throw its own attributes on the objects being iterated over. If the array that you give to ng-repeat is a seamless-immutable array you’ll get a runtime exception because Angular is trying to add $$hashKey to it behind the scenes.

While that was a frustrating discovery, it’s unfair to fault seamless-immutable for Angular’s quirks and you can simply call asMutable() on your immutable array to convert it to a mutable Javascript array. As a side note, you would already have to do the equivalent in ImmutableJS.

Persistent Data Structures

As I mentioned earlier, this characteristic was the least important to us. However, having structural sharing within an immutable object would be a bonus.

While seamless-immutable cannot and will not have persistent data structures, we ended up satisfying this wish list item after all because of Redux.

Let’s take a look at Redux to understand why.

  • Redux operates on the idea that there is one giant application state driving your views.
  • Redux expects you to provide a reducer function that takes the previous application state, an action, and produce a new state if a change was made. Otherwise the original state reference is returned.
  • Redux reducers are composable. That is to say, I can assign smaller reducers to manage smaller parts of my state tree.

Even though our entire application tree is a seamless-immutable object, only one or two small chunks of the tree change reference for any given action. One or two reducers may return a new reference for an action, but most reducers will return the original part of the entire state they were managing. So instead of getting full blown structural sharing, we get chunk sharing where within those individual chunks there isn’t any structural sharing.

Is seamless-immutable better than ImmutableJS?

In summary, is seamless-immutable better? I don’t know. That kind of question doesn’t make sense to ask, in my opinion.

A better question would be, what are the trade-offs between these two immutability solutions?

We defined the criteria for an acceptable immutability solution and seamless-immutable came out on top. ImmutableJS is backed by a tech giant, has a large community in comparison, and uses persistent data structures, but those weren’t as important to us. From that regard, we think seamless-immutable is worth checking out as a possible alternative to ImmutableJS.