Replacing state in Redux reducers: a few approaches

by
Tags: , ,
Category:

The Redux state management engine relies on returning new state object instances whenever a state change is made. Since your components re-render whenever their state or props are replaced, this makes rendering easy. But how do you best go about managing the state inside the store, and making sure you create a new instance each time? I'm going to give you a data structure to work with, and techniques to manage and update that structure, so you can see how its done.

A sample redux state tree

Here's a graph of data we can manage in a Redux store:

const initialState = {
    name: 'Player One',
    score: 100,
    weapons: {
      'laser': { damage: 10, description: 'Laser Blaster' },
      'hammer': { damage: 5, description: 'Rusty Hammer' }
    },
    currentWeapon: 'laser',
    goalsAchieved: [
      'foundMap',
      'openedDoor',
      'killedBossCharacter'
    ]
};

The challenge: Updating a small portion of the state

Let's say you have to add a new goal to your state tree, and increment the score by 100 points.

This would result in two changes to the tree.

Option 1: clone the object state with a collections API

You could manually construct a new object, using lodash#mergeWith for example to clone the state to a new copy and then appending your piece to it. This is good if you're stuck in the ES5 world:

// from LoDash Documentation - a custom merge strategy to concat arrays
function customizer(objValue, srcValue) {
  if (_.isArray(objValue)) {
    return objValue.concat(srcValue);
  }
}
var newState = _.merge(state, { score: 200, { goalsAchieved: ['ateSandwich'] }, customizer);

This would require you to install lodash, which is not the worst thing you could do. It's a great library and if you're processing collections regularly it is a great go-to.

Option 2: Using ES2015+ Object.assign

The Object.assign function, beginning in ES2015, was provided to make it easy to merge objects together. It processes by starting with an initial object, and then folding all other objects passed into the object, overwriting any prior keys. So, in our example above, we could do:

const newState = Object.assign({}, initialState, { score: 200,
  goalsAchieved: initialState.goalsAchieved.concat('ateSandwich')});

Note the use of an empty object in the first parameter of the Object.assign function. This creates a new object. We then use the initial state as the first object to fold into our new object, and follow it up with another object to overwrite the score, and a concatenation of our new goal with the current array of three.

Option 3: Use ImmutableJS

This is a more complex option, but with the possibility of more power. ImmutableJS creates immutable structures from JavaScript objects. In the Immutable world, you create your state like this:

const state = Immutable.fromJS({
    name: 'Player One',
    score: 100,
    weapons: {
      'laser': { damage: 10, description: 'Laser Blaster' },
      'hammer': { damage: 5, description: 'Rusty Hammer' }
    },
    currentWeapon: 'laser',
    goalsAchieved: [
      'foundMap',
      'openedDoor',
      'killedBossCharacter'
    ]
});

From there, any change to state is done via a set method:

const newState = state.set('score', 200);

If you want to change something deeper in the path, you can use setIn:

const newState = state.setIn(['weapons', 'laser', 'description'], 'Broken Laser Blaster');

If you want to make multiple changes, you can use withMutations:

const newState = initialStateI.withMutations(state => {
   state.set('score', 200);
   state.set('goalsAchieved', state.get('goalsAchieved').push('eatSandwich'));
});

To get a value out of the immutable state, you call get or getIn:

const score = state.get('score');   // primitive, fetched as JS
const goalsAchieved = state.get('goalsAchieved').toJS();   // must convert to JS array
const laser = state.getIn(['weapons', 'laser']).toJS();    // must convert to JS Object

If you leave out toJS(), you'll get an immutable sub-structure. To work with the data in the component, often you'll need to finally convert to JavaScript, but to manage state, you'll leave it in the immutable context.

The challenge with ImmutableJS is that moving in and out of the immutable structures can be complex, and suddenly everything inside of the structure is more difficult to debug and harder to reason about.

Option 4: Spread operators for new State in JavaScript

Now that we can use array spread syntax in ES2015 (and object spread using a newer addition to the language), lots of this complexity melts away. To wit:

const newState = {
   ...state,
   { score: 200 },
   goalsAchieved: [...state.goalsAchieved, 'eatSandwich']
}

This was a takeaway from a conversation with another developer at my talk recently. I was really pushing my way through using ImmutableJS as a way to reason about Redux state. However, most of my bugs involved the nested structures inside of the Immutable objects. but with the spread operators, we can just keep everything pure JavaScript. And by using an object spread to create the new object in the first place based on the old properties, we get our shallow object reference change, which makes React happy.

Conclusions

If you're starting off with a new React Redux project, use the create-react-app starter, which has object and array spread operations in place, to manage your state as in option #4 above. If you're stuck with ES5, consider using LoDash to help you manage state.

I'd avoid Object.assign unless you're stuck with an older Babel transformation library. Though it has been out there as a canonical syntax for a while, the spread operators really reduce the complexity a ton.

Use ImmutableJS or another immutability library if you really have complex structures and want to take advantage of the features it provides. Keep in mind you're getting into a complex world once you embrace libraries like that.

I hope this helps you if you're beginning your road to implementing Redux in your projects. Let me know what you think in the comments.

The Plug – React and Redux Training

Since you've read this far, and hopefully you found the article useful, consider
attending our React and Friends training course.
Offered onsite at your location, for your employees, we take you from the basics
of building a React application through calling RESTful APIs, dealing with forms,
handling state in props/state and Redux, and learning how to use the React Router.