Optimizing Redux Components

In part one of Riipen’s three-week series on how to optimize React components, we explored React’s virtual DOM and antipatterns that spoil the reconciliation process.

This week, we’ll take a look at how you can avoid that process altogether if you already know your component doesn’t need to be re-rendered (for example, if its output only changes when particular properties are modified). We’ll also explore how to apply what we learned last week to components connected to the Redux store.

Redux-specific optimizations

Once you’ve got your component rendering without creating redundant object references, the next step in optimization is to take a look at what happens in your component before it renders — that is, before React has had an opportunity to reconcile changes. This code may be run every time the state is updated, even if your component is returning a component tree that can be easily reconciled (without the gotchas mentioned in our previous post).

Before we dive in, take a look at the React documentation to familiarize yourself with the component lifecycle.

The Same Rules Apply for State Mapping Functions

If you’re using Redux, be mindful of what connected components are doing before they render. Mapping functions like mapStateToProps might be doing a lot of work every time the state is updated, even if React ultimately discards the render output of the connected component.

As with render, avoid creating callbacks and other functions in mapping functions, especially if those variables will be passed in as props to other components. Instead, use mapDispatchToProps or take advantage of the fact that, by default, connect passes dispatch to your component as a prop. That means you can create component methods that use this.props.dispatch as a function instead of creating new functions every time the state changes — which can happen quite frequently, especially if you are using a package like Redux Form.

No changes? Skip Rendering Altogether

In some cases, even if React is able to discard the output of render most of the time, the method still ends up doing a lot of unnecessary work. You can skip rendering entirely by implementing shouldComponentUpdate. This is a simple method that receives nextProps and nextState as arguments and returns true if render should be called. If it returns false, React won’t render the component or reconcile the result with what has already rendered.

In most cases, in a Redux app, shouldComponentUpdate compares only nextProps because any variable state changes have already been selected for the component in mapStateToProps.

It’s best to implement shouldComponentUpdate only if you are very certain about the conditions under which your component needs to render. For example, if you are outputting a list of entities and know that your component only changes when they are modified in the Redux store, you can implement this method with confidence that your component won’t be outdated by other state changes.

See Francois Zaninotto’s blog post at Marmelab for a great example of refactoring components to make good use of shouldComponentUpdate.

Tips

  • Your implementation needs to be speedy, ideally doing a direct comparison of specific properties you know to be variable; otherwise, you could end up bogging down the component in your effort to optimize it!
  • A good rule of thumb is for your implementation to be more no more complex than PureComponent.shouldComponentUpdate, which does a shallow comparison on state and props (see below).
  • If your component can make use of shouldComponentUpdate, move as much logic as possible into the render function so that it can be avoided if the component doesn’t need to update.
  • Be aware that future versions of React may interpret the results of shouldComponentUpdate as a hint rather than a directive.

Consider React.PureComponent

Whereas React.Component implements a default shouldComponentUpdate method that always returns true, PureComponent implements a shallow prop and state comparison. It can be a shortcut to a custom shouldComponentUpdate implementation, but the same rules apply as to general reconciliation: mapStateToProps and mapDispatchToProps can easily foil the comparison by passing in new references to the same data on every call.

That said, using a library like Reselect, that memoizes Redux selectors, will prevent new references being created from simple data selections.

Generally, using PureComponent should be the conclusion of your optimization rather than the starting point. Defaulting to it will actually slow down your application because React could end up doing a double diff on every render: first of the props and state and then (if they are different) of the virtual DOM returned by render.

If you decide to extend from PureComponent, have a clear idea of what property and state changes you need to watch out for and why a custom implementation of shouldComponentUpdate would be redundant.

Skip Redux Property Mapping

Just as React lets you skip render with shouldComponentUpdate, React Redux allows you to skip mapStateToProps and mapDispatchToProps. The connect decorator accepts options as a fourth parameter, which is an object that can have the following properties:

  • areStatesEqual(next, prev)
  • areOwnPropsEqual(next, prev)
  • areStatePropsEqual(next, prev)
  • areMergedPropsEqual(next, prev)

Each function is of course optional and simply returns true or false. See the React Redux documentation for details on how to use these options.

If your mapStateToProps function does something more complex than simply select data from the store, and which you can’t move to the render function, consider implementing one of these tests. In the vast majority of cases, however, they shouldn’t be necessary.

Although optimizing Redux applications can seem overwhelming at first, the same principals apply to them as regular React apps. Having an understanding of what’s happening behind the scenes yields a few simple things to be mindful of. And, with judicious use of hooks like shouldComponentUpdate, you can always work around the default behaviour to speed up more complex code.


In the final part of this series published next week, we’ll take a look at some tools you can use to determine what components in your app would most benefit from these techniques and how you can measure the effectiveness of your optimizations.

Stay Riipe!