Immutability in JS

For the last several months nearly all of my front end code has been done with React, Redux, and Immutable.js. Over this time I've never really enjoyed using Immutable.js. While it's a fantastic library and fits in perfectly with Redux, it also has never felt right to me. I've always felt that there had to be a more JavaScript-y way to do things. Why not devise a way where I can simply access the properties in a way that is standard to JavaScript?

For readers unfamiliar with Redux or Immutable.js:

  • Redux is a state management library that allows you to more effectively move data around an application.
  • Immutable.js is a library that makes it nearly impossible to accidentally modify an object. Immutabe.js comes with the added benefit of returning an entirely new object each state change allowing for fewer re-renders in React.

So I decided rather than continuing to sit around and moan about it, I should try to solve the problem. I fumbled around with a handful of ideas including everything from closures (still thinking about this one. I kind of like the idea) to immutability by practice (could never work, devs aren't to be trusted!). After a week or so of mulling this around I finally decided on getters and setters.

As getters and setters are pretty uncommon in the JS world, I'm going to give a quick explanation. Feel free to skip on ahead if you know this part!

Getters and setters allow you to handle how object properties are accessed a little different than usual. For example:

var example = {
    val: undefined,
    get alwaysDoubled() {
        return this.val;
    },
    set alwaysDoubled( input ) {
        this.val = input * 2;
    }
};

// This accesses the setter, setting the val prop to 10.
example.alwaysDoubled = 5;

// Prints out example.val
console.log( example.alwaysDoubled ); // 10

As you can see from the example above, getters and setters allow us to interact with objects like normal, but handle the data differently.

Once I had decided on getters and setters I was surprised to find the majority of my problem solved in a single Stack Overflow post. I didn't even have to use ES6 computed property names (which I honestly didn't realize worked with getters/setters until writing this post)! Let's look a little deeper.

function makeReadOnly( cloned, obj, prop ) {
    // Using Object.defineProperty because we need to set both the getter and setter.
    Object.defineProperty( cloned, prop, {
        set( val ) {
            // Any attempt to set a new value for the property ( obj.prop = 1 )
            // will throw an error.
            throw new Error( `Cannot assign value '${ val }' to read only property '${ prop }'.` );
        }
        , get() {
            // Simply returns the value.
            // console.log( object.prop ); // whatever the value is.
            return obj[ prop ];
        }
        // Allows looping over this property. Remember, we want this to feel 
        // like a normal object in all aspects but mutability.
        , enumerable: true
    } );
}

With the aspect of reasonable immutability out of the way, I just needed to make it fit the Redux architecture best by returning a new object each state change. I decided the best way to make this happen was by using inheritance, I wanted it to be nice and easy to do a state change by just calling state.newState( nextStateObject );. My first shot at this involved the user setting up their initial state as a class that extended the state manager, but that felt like more set up than was necessary. I realized I could make things easier for developers by using Object.create. By handling the inheritance on my side I could make set up as easy as

const initialState = new StateManager().newState( initialStateObject );

and a state change as easy as only passing in only the properties that changed!

Here is the final code I ended up with for the project as well as some examples:

class StateManager {
    newState( changed ) {
        if ( typeof changed !== `object` || changed === null ) {
            throw new TypeError( `state must be an object. Instead got ${ typeof state } : ${ state }` );
        }
        const nextState = Object.create( this );

        for ( const prop in changed ) {
            this.makeReadOnly( nextState, changed, prop );
        }

        return nextState;
    }

    // http://stackoverflow.com/questions/20730324/how-to-make-dynamic-getter-with-defineproperty-in-javascript
    makeReadOnly( cloned, obj, prop ) {
        Object.defineProperty( cloned, prop, {
            set( val ) {
                throw new Error( `Cannot assign value '${ val }' to read only property '${ prop }'.` );
            }
            , get() {
                return obj[ prop ];
            }
            , enumerable: true
        } );
    }
}

const initialState = {
      emails: [
          { from: 'mom', message: 'Saw this and thought of you!' }
          , { from: 'bossman', message: 'Are you playing with open source instead of working again?' }
          , { from: 'prince alotik', message: 'I am a deposed nigerian prince and with your help...' }
      ]
    , selectedEmail: {}
};
const firstState = new StateManager().newState( initialState );

console.log( firstState.emails );
const secondState = firstState.newState(  { selectedEmail: { from: 'prince alotik', message: 'I am a deposed nigerian prince and with your help...' } } );
console.log( secondState );
console.log( firstState === secondState); // false
// firstState.emails = 1; // Throws an error

So why did I do this?

Well the biggest pro for me was I don't have to use Immutable.js syntax, I can treat my state like a normal object. Being able to pass in only the properties on state that changed is nice too. Its also only ~30 lines of code. But mostly I did this because I love playing with code and this introduced me to some topics I've never really played with in JavaScript.

I whipped up a super simple (and super ugly) proof of concept that is available on GitHub. As always I would love to hear your thoughts, questions, concerns, insults, and ideas on Twitter or LinkedIn!

First Foray Into TypeScript