Techniques for Immutable Objects
Adam Klein
Last updated on Dec 5, 2017

The examples used in this tutorial are using `lodash/fp`, but are also applicable to other approaches, such as using ES6, ImmutableJS, seamless-immutable, Rambda, etc. If you are completely unfamiliar with `lodash/fp`, you can use this guide to get started.

Currying means that if you don't pass all the parameters, you get back a function that's bound to the missing parameters. We will be using this concept throughout the examples.

The following are equivalent:

set(key, value, obj)
set(key, value)(obj)

Updating an item in an array was always one of the tasks that immutability sucked at. Assume we have the following object:

state = {
  arr: [
    { id: 1, key: 'value1' },
    { id: 2, key: 'value2' }
  ],
}

And we want to change the key of the item with id === 2. With mutable objects you can simply use the native .find method:

arr.find(i => i.id === 2).key = 'newValue'

But with immutables, you have to create a new item out of the old item, then replace it inside the array, and then replace the array inside a higher-order object. If you're not using any library, it might look like this:

const newArr = state.arr.map(item =>
  item.id === 2
    ? ({ ...item, key: 'newValue' })
    : item
)

return { ...state, arr: newArr }

There are other approaches as well, but all of them are tedious to write and hard to read.

Using lodash/fp you can get to a much more elegant solution:

import { findIndex, set, update } from 'lodash/fp'

const index = findIndex({ id: 2 }, state.arr)

return set(['arr', index, 'key'], 'newValue', state)

You might get carried away with functional programming and write code that looks like this:

update('arr', flow([
  map(flow([
    update('count', (count) => count + 1),
    set('visited', true)
  ])),
  filter((item) => item.count < 10)
]), state);

Needless to say, this code is completely unreadable by other team members, and probably even by the original coder.

We can store and pass bounded functions to other lodash functions, and avoid over-nesting function calls. For example:

const toggleChecked = update('checked', checked => !checked)
const setDisabled = set('disabled', true)

return flow([toggleChecked, setDisabled])(state)

So, for the initial example, it can be re-written like so:

const isItemLarge = item => item.count < 10
const visit = flow([
  update('count', count => count + 1),
  set('visited', true)
])

return update('arr', flow([
  map(visit),
  filter(isItemLarge)
]), state)

One must take caution not to over extract functions as well. Jumping back and forth between functions and files to understand the flow also hurts readability. So you should find the right balance between the two.

Back to all articles

© 500Tech. Building high-quality software since 2012.