3 small tips for better Redux performance in a React app

Aggelos Arvanitakis
ITNEXT
Published in
7 min readMar 7, 2019

--

In my previous article I went through some core concepts of performance optimisation in React apps. This article will extend these concepts by focusing on practices which will make sure that the introduction of Redux into your project is not causing any perf bottlenecks as your app grows.

Redux itself is simply an event emitter as Andrew funnily pointed out. Thus, all possible optimisations are automatically delegated to React itself. In React, we translate optimisations into rendering time reduction and reduction of overall renders. The former has to do with the amount of work that is performed during the render phase of a component (expensive computations, multiple function invocations, custom DOM mutations, etc.), while the latter with the number of times that a component has rendered. The amount of work inside a React component is strongly tied to the business logic and is most likely unrelated to redux, but the number of renders can easily be relevant to redux and can gradually increase if you don’t take care.

So, here we go:

1. Make use of memoization in your selectors

Selectors are nothing new in redux, they are simply a way of abstracting the actual state implementation from the component that’s reading it; a proxy between your component and the actual redux state. Writing a selector can be as easy as this:

A simple redux selectors for the `items` key

In the above example, we assumed that the data were stored in a list. Redux docs though, recommend that state is normalized. That means that we should store the items in an object where the keys are the IDs of the items and the values the items themselves. In order to achieve a similar result as the example above, we would then naively rewrite a selector like that (assuming that order of items doesn’t matter):

// remember, items is an object in this case
export const getItems = state => Object.keys(state.items);

This implementation actually serves our purpose, but has one big flaw. Any time any piece of the Redux state is updated, our Component will re-render, regardless of whether the store update was related to our component or not. You see, when you connect a component using react-redux’s connect() HOC, Redux re-calculates the output of the mapStateToProps that has been supplied to the connect() and performs a shallow equality check. If the props before and after the store update are referentially the same, then the component does not re-render. If they are different though, then the component will re-render.
In our case, Object.keys returns a different instance every time it is invoked. Thus, since this selector is within our mapStateToProps, it will get re-evaluated every time a store update is performed. Just because the reference of this array will be different than its previous one, react-redux will falsely think that the props of the component are different and so it will “allow” React to re-render.

Operators like .map and .filter return referentially different values every time they are invoked. Thus, we must make sure that when the actual output is the same, then the reference stays the same and when the actual output is different, then the reference should be different as well. To do that, we employ a technique called memoization. This technique guarantees that “as long as the inputs to a function are the same, then the output will be referentially the same”. One popular library option to implement that is reselect. With it, we could re-write our selector like so:

import { createSelector } from 'reselect';export const getItems = 
createSelector(state => state.items, items => Object.keys(items))

The createSelector accepts an arbitrary number of function arguments, where the first N-1 functions are “inputs” to the selector and the Nth one is the “output”. It guarantees that as long as the inputs have the same value, the output will be “cached” and re-used. If any of the inputs has a different value compared to the value it had during the last selector invocation, then the “output” from the Nth function will be re-computed. In our example, as long as the items remain the same, then the getItems selector will return a value with the same reference, no matter how many times it’s called. If new items are added, then the items state won’t have the same value anymore, so the getItems will return a new reference which will be cached and re-used.

As a rule, anytime you need to perform an operation that returns a new reference on every selector invocation, make sure you memoize the result (Tip: you can use a memoization selector inside another memoization selector when dealing with complicated selector logic).

2. Use referentially consistent action creators

Sometimes action creators are simple functions like so:

const fireAction = () => ({ type: 'ACTION', payload: {} });

In the above case, fireAction has a fixed reference, but unfortunately that’s not always the case. There are times where you need to pass the component’s props to your action creators in order to differentiate the data passed to your reducer. For example, let’s take a look at the following case:

action creator using component’s props

The above implementation is a bit naive. We want to “bind” the fireAction action creator to the particular item that MyComponent has as prop. To achieve that, we “read” the props from within mapDispatchToProps and use them to create fireActionWithItem. What some people don’t know, is that whenever you use the component’s props from your mapDispatchToProps (whenever you use the 2nd argument of this function), react-redux will recalculate the output of mapDispatchToProps each time the component’s props change. Thus, if any prop from MyComponent were to change, then mapDispatchToProps would run again. Because of the nature of anonymous functions, the fireActionWithItem that is passed as a prop to MyComponent would be referentially different every time . That’s not a problem for the component that is rendering (since it would re-render regardless of what happened inside our mapDispatchToProps), but it might create issues for other components declared inside MyComponent that get fireActionWithItem as prop. For example, let’s take a look at the following scenario:

Expensive Component re-renders for no reason…

We have 2 components: MyComponent and ExpensiveComponent . As you can see, ExpensiveComponent is wrapped in a memo() so it will only re-render when its props change. Each time the randomProp in MyComponent changes, then the mapDispatchToProps will be re-calculated, the fireActionWithItem will get a new reference, MyComponent will re-render and ExpensiveComponent will be forced to re-render as well since its onClick prop will be different. Ultimately, ExpensiveComponent shouldn’t have re-rendered at all since nothing that affects it has actually changed.

To combat scenarios like that, you can either delegate the firing of the action with the correct params to the component itself (by passing both item and fireAction as props to the component), allowing it to “create” the fireActionWithItem with a fixed reference (by making use of .bind & useCallback), or you can use memoization techniques in your action creators. For the latter, you can employ the same concepts as the ones you employed in selectors and make sure that the reference is always the same. Both options can be seen in the gist below:

Two approaches for referentially consistent action creators

3. Batch actions to reduce # of renders

Whenever you have to chain multiple actions one after the other (which you intentionally keep separate for separation-of-concerns in your actions) , you would be better off batching them and firing them with a single dispatch. Grouping these actions can guarantee that instead of X potential re-renders, there will only be 1. There is a caveat though; in order for the actions to be “batch-able”, they need to be independent of one another. That means that the N action must be independent of the state created by the N-1 action. In other words, you should be able to change the firing order of these actions without affecting the end state. If that’s the case, react-redux 7.x.x offers a new batch function that helps you update your state with only a single dispatch, saving you the additional — potentially costly — renders.

import { batch } from 'react-redux'
import { action1, action2 } from './actions';
// dispatches both actions at the same time
batch(() => {
dispatch(action1());
dispatch(action2());
});

In order to use it would need access to the dispatch function. There are lots of ways to achieve that, depending on the where the actions are fired from. For example, you can get access to dispatch from your mapDispatchToProps function, from a redux-thunk thunk or from manually importing the store and using store.dispatch. It mainly boils down to how your app is structured. It has to be said that this technique is rarely used, but can yield performance gains in cases where freedom of the main thread is crucial. If, for example, you were performing heavy animations after firing these actions, then a single dispatch might save you precious frames.

Conclusion

I intentionally haven’t touched the topic of correctly organising your state which is even more crucial than the above tips. The purpose of this article was to shed light to potential performance losses through non-optimal integration of actions-creators & selectors in a React app. Under no means do I believe that these concepts should be used by all projects — since for most of them they are an overkill — but they are good to know.

Thanks :)

P.S. 👋 Hi, I’m Aggelos! If you liked this, consider following me on twitter and sharing the story with your developer friends 😀

--

--