Skip to content

Instantly share code, notes, and snippets.

@stockwellb
Last active June 19, 2017 00:36
Show Gist options
  • Save stockwellb/13aeaf95e317c205289d13b9052c2984 to your computer and use it in GitHub Desktop.
Save stockwellb/13aeaf95e317c205289d13b9052c2984 to your computer and use it in GitHub Desktop.
The Immutable Fab Four: Strategies for immutable JavaScript

The Immutable Fab Four

Strategies for immutable JavaScript

Immutable data is a concept that I had to deal with while writing my first reducer for ngrx/store. The idea behind immutable data is that it cannot be changed after creation. This simple shift in state management can make applications more simple to build and easier to debug. With a single source of truth you have a contract in hand that will free you of a whole class of problems that plague mutable applications. I’ll share what I’ve learned so far. Keep in mind that these examples stem from my experiences using ngrx/store but are in no way directly tied it.

Let’s start with some state: This state will consist of an object with an array of ids and an entities object that will store one key/value for each id in the ids array.

let states = [];
const initialState = {
  ids: [],
  entities: {}
}
states = [...states, initialState]

Adding some data to our empty state:

// add john
const john = {id: 1, name: 'John Lennon'};
const solo = {  
  ids: [...initialState.ids, john.id],
  entities: Object.assign({}, initialState.entities, {[john.id]: john})
}
states = [...states, solo];

There’s a lot going on here. We’ll look at one thing at a time. john is the new object that we want to add to the state.

const john = {id: 1, name: 'John Lennon'};

From the object john we’ll extract the id property to add to the ids array and copy the id and name properties into the entities object.

const solo = {
  ids: [] //add new ids,
  entities: {} //add new entities
}

Adding new ids: The spread operator is used to expand any existing ids in place, followed by the new object’s id. The solo.ids array will have no references to the initialState.ids.

ids: [...initialState.ids, john.id]

Adding new entities: The Object.assign method is used to create a new entities object. The first argument is a newly created object followed initialState.entities. The key/values from initialState.entities will be shallow copied into the new empty object. Lastly, the object {[john.id]: john} will be merged with the newly created object.

entities: Object.assign({}, initialState.entities, {[john.id]: john})

In the end the state solo will look like this.

{
  ids: [1],
  entities: {
    1: {id: 1, name: 'John Lennon'}
  }
}

Adding more data: The others array contains new objects to be added to the state. We’ll need to extract the ids from array and reduce the objects in it to one single object.

// add paul, george and ringo
const others = [
  {id: 2, name: 'Paul McCartney'},
  {id: 3, name: 'George Harrison'},
  {id: 4, name: 'Ringo Starr'}
]; 
let newIds = others.map(x => x.id);
let newEntities = others.reduce((acc, member) => {
  return Object.assign(acc, {[member.id]: member});
},{})
const fabFour = {  
  ids: [...solo.ids, ...newIds],
  entities: Object.assign({}, solo.entities, newEntities)
};
states = [...states, fabFour];

First the ids: newIds = others.map(x => x.id) creates a new array named newIds that has no reference to the others array. Next combine newIds with solo.ids by using the spread operator like this. ids: […solo.ids, …others.map(x => x.id)].

The entities will take a little more work: reduce and Object.assign are used together to shallow copy all the members of the others array into a single new object called newEntities. As before, there are no references to the old data. The new data has been copied. No mutations have occurred.

let newEntities = others.reduce((acc, member) => {
  return Object.assign(acc, {[member.id]: member});
},{})

Assembling the state: It’s as simple as copying the existing data and the new data into a new object. The spread operator is used to expand the existing solo.ids and the newIds , which are combined together into the fabFour.ids array. Object.assign is used to merge the existing key/value pairs of solo.entities and newEntities into the new empty object {}.

const fabFour = {  
  ids: [...solo.ids, ...newIds],
  entities: Object.assign({}, solo.entities, newEntities)
};

Adding more data: With some data in the state, let’s take another look at adding a new object. This should look very familiar. This is the same process used to add john to the earlier state.

//add billy
const billy = {id: 5, name: 'Billy Preston'};
const fabFourV1 = {
  ids: [...fabFour.ids, billy.id],
  entities: Object.assign({}, fabFour.entities, {[billy.id]: billy})
}

Removing and replacing data: Let’s remove billy and replace it with eric . The strategies are the same as we’ve used so far. First, filter the ids to newIds. newIds will no longer contain billy.id. Next, reduce newIds down to the newEntities object looking up each item from the newIds array in the fabFourV1.entities object. In the end newEntities will no longer contain the billy object. Finally, merge eric.id with the filtered newIds and merge eric with the filtered newEntities.

//remove billy and replace him with eric
const eric = {id: 6, name: 'Eric Clapton'};
newIds = fabFourV1.ids.filter(x => x != billy.id)
newEntities = newIds.map(id => fabFourV1.entities[id]).reduce((acc, member) => {
 return Object.assign(acc, {[member.id]: member});
},{});

fabFourV2 = {
  ids: [...newIds, eric.id],
  entities: Object.assign({}, newEntities, {[eric.id]: eric})
}

There have been 5 states created in this exercise; initialState, solo, fabFour, fabFourV1, fabFourV2. Each state was created with immutable methods. There are no references shared between them.

for (var i = 0, len = states.length; i < len; i++) {
  console.log(states[i].ids.map(id => states[i].entities[id].name));
}

This output shows that each state is a separate object.

[]
["John Lennon"]
["John Lennon", "Paul McCartney", "George Harrison", "Ringo Starr"]
["John Lennon", "Paul McCartney", "George Harrison", "Ringo Starr", "Billy Preston"]
["John Lennon", "Paul McCartney", "George Harrison", "Ringo Starr", "Eric Clapton"]

No states were mutated during the creation of this article. The full source can be found here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment