Using Ramda’s evolve in Redux reducers to create new state


Ramda is a JavaScript utility library similar to lodash or underscore ((I haven’t used underscore in ages though)) with an extra touch – it’s designed to be better suited to functional style of programming. For example it provides automatic currying ((Remodeling function in a way that instead of accepting multiple arguments at once it accepts them one by one. Useful when pre-loading with known arguments or when composing functions that shares inputs and outputs – output of previous function is input to the next one.)) for all of its functions. But what I like most about Ramda is that it never mutates user data. Which makes it super well suited for applications that heavily relies on immutability (in my case applications that use Redux for managing their state). And in a process it makes code cleaner.

Take a look at the following example. It’s a reducer that adds, removes a group of contacts and adds, remove contacts from group. Every group has a property contactIds that ties contacts from other part of the store to particular group. When group is removed, its contacts are transferred to default group. I’m also using normalizr library when adding groups to keep groups in store IDed (not the point of this post so just play along if you don’t know about this library).

export default (state={}, action={}) => {
  const {type, payload} = action
  switch(type) {
    case REMOVE_GROUP:
      let groups = filter(state, (g, id) => id != payload.id)
      groups[0].contactIds.push(state[payload.id].contactIds)
      return groups
    case ADD_GROUPS:
      const data = normalize(payload.groups, [new schema.Entity('groups')])
      return Object.assign({}, state, data.entities.groups)
    case ADD_CONTACT_TO_GROUP:
      groups = {...state}
      groups[payload.groupId].contactIds.push(payload.contactId)
      return groups
    case REMOVE_CONTACT_FROM_GROUP:
      groups = {...state}
      groups[payload.groupId].contactIds = filter(groups[payload.groupId].contactIds, id => id != payload.contactId)
      return groups
    default: return state
  }
}

I’m fine with code in first two cases, but the last two cases could use an improvement so that I wouldn’t have to clone the state object and then specify same nested contactsId twice.

If you are familiar with Immutable than you’d probably think of its setIn and than as second parameter provide a updater function which gives you previous property’s value to operate with. You also need to convert state object to a Immutable data structure (Map, List, Record) beforehand so you can use that setIn. Not to mention sometimes you need to convert back and forth to plain JavaScript objects when using outside of reducers. Man, that extra work really doesn’t make me enthusiastic about Immutable (even if it’s written and managed by Facebook).

Equivalent in Ramda is using evolve. Evolve takes two parameters. First is transformation object that mimics our state object but only contains properties that we want to update/evolve. Their values are transformation functions that takes value of that property as an input. Second parameter to evolve is our data to be transformed.

Below you can find refactored snippet using evolve. There wasn’t a need for any custom-made transformation functions because one adds an id to list, other removes it and there are Ramda’s append and without for that.

export default (state={}, action={}) => {
  const {type, payload} = action
  switch(type) {
    //...
    case ADD_CONTACT_TO_GROUP:
      return R.evolve({
        [payload.groupId]: {
          contactIds: R.append(payload.contactId)
        }
      }, state)
    case REMOVE_CONTACT_FROM_GROUP:
      return R.evolve({
        [payload.groupId]: {
          contactIds: R.without(R.of(payload.contactId))
        }
      }, state)
    //...
  }
}