Easier Immutable Updates

October 24, 2017 by Robert Katic

When using Redux for state management of applications, the importance of data immutability is generally well understood. Every state update means replacing it with a new one. However, it’s important to reuse unchanged portions of the previous state as much as possible. Through this reuse, it becomes possible (via simple identity checks) to avoid unnecessary re-computations (via selectors) and to ensure minimal re-rendering (for example by using React’s pure components).

Also, in Redux, it’s recommended to keep your state “flat” by normalizing your models. This makes it possible to break state updates into multiple, simple and composable reducers.

Normalized complex data, however, easily becomes difficult to manage and you will find yourself looking at some of the ORM solutions, or you even consider dropping Redux entirely and trying something like mobx-state-tree.

Another option is to simply leave some parts of your state de-normalized, and use a utility to make “deep” updates easier. Although generally not a wise approach, it has its merits — one of those is a more gradual learning curve.

You’ll likely end up having somewhat large reducers, but if done right, with a proper utility, updates should remain simple and concise.

This last approach is common enough that npm is full of packages for immutable updates, where you could find one that you like, and that fits your project well. However, different libs have different sets of features, and ways of defining updates. One can be really useful for one project, but not so much for another.

The Update lib

Ideally we would like a small lib, which allows different features/approaches that work well together, and is powerful enough to make even complex updates a breeze. Consider a simple state:

const state = {
  users: {
    0: {
      id: 0,
      name: 'Bob',
      valid: false,
      balance: {
        amount: 123,
        currency: '$',
      },
      paymentMethods: [...]
    }
  }
}

By just using the spread operator, you’ll write something like:

const newState = {
  ...state,
  users: {
  ...state.users,
    0: {
      ...state.users[0],
      valid: true,
      balance: {
        ...state.users[0].balance,
        amount: state.users[0].balance + 100,
      }
    }
  }
}

Even from a simple example like this, we can see it gets ugly very quickly. With a “patch” approach we can write a much cleaner code:

const newState = update(state, {
  users: {
    0: {
      valid: true,
      balance: {
        amount: n => n + 100,
      }
    }
  }
})

This is useful for changing multiple values, but less elegant with really deep structures. The “path” approach can really help to reach even deeper values:

update(state, 'users.0.name', 'Alex')

update(state, 'users.0.balance.amount', n => n + 100)

The “patch” and the “path” approach can work really well together too:

const newState = update(state, 'users.0', {
  valid: true,
  balance: {
    amount: n => n + 100,
  }
})

Removing

A fairly common task is to remove items from an array or object. It is not hard to imagine usefulness of a remove utility function that will not mutate original data. However, this would not play too well with our update function, especially if multiple changes are involved.

By introducing a special REMOVE value, we can simply mark items for removal without loosing flexibility of existent approaches:

const newState = update(state, 'users.0', {
  valid: true,
  paymentMathods: {
    0: REMOVE,
  }
})

// remove entire user
const newState = update(state, 'users.0', REMOVE)

Advanced features

All the above features, and more, are implemented in a small lib. Some more advanced features are shown in the following snippet.

// Replace $ with €
update(state, 'users.*.balance', balance => {
  if (balance.currency !== '$') {
    return balance
  }
  return update(balance, {
    currency: '€',
    amount: n => n * euroDollarRatio,
  })
})

// Remove VISA as method of payment from certain users
update(state, ['users', userIds, 'paymentMethods', {type: 'VISA'}], REMOVE)

If you are interested in innovating and growing your product, let's do it together. Reach out to us today.

© Blazing Edge 2018