Be Selective With Your State
A Dive into Selectors featuring reselect
and re-reselect
.
This article focuses on selectors in the context of a React
application backed by a redux
store. If you or your friends are writing a React
application with redux
and like to do things right.. this article is for you!
What is a selector anyways? If a redux
store is kind of like a database then selectors are like queries. And just like you would normalize a database you should be storing minimal state in your redux
store. One problem with minimal state is that the derived data your components depend on can be computation intensive or complicated to get from the minimal representation. Selectors can solve all these problems and more. Selectors are such a central part of application data flow that at Riipen we decided to audit our selector usage. We found that we were not getting any benefit from the way we used reselect
and that needed to change.
Lets explore the motivation for using selectors. One common usage of selectors is to cache expensive computations. This way the value won’t get recomputed until the underlying relevant state changes. Another usage is to return the exact same (===) array or object on a computed set of data. This is useful to keep React
from needlessly re-rendering a component. An example of this is below.
This component re-renders sooo often.
In the example above the component will re-render every time it receives new props. Changing state.someArray.filter(Math.isEven) to someArrayEvenSelector(state) with a proper implementation of the selector means the component doesn’t re-render until the underlying state changes. With thousands of components and functions more intensive than isEven this performance benefit can add up quickly.
Note that reselect
and redux
are built on the idea of immutable state. Mutating objects in redux
state will break reselect
and cause your selectors not to recompute when they should!
Lets look at some code!
What is reselect
? reselect
is a simple selector library for redux
. The classic example is using a selector to calculate total price based on some items in a cart and a tax rate. Lets deviate a bit and break down a simplified real-world example from Riipen.
First you need some context. Here is the shape of our redux
store and an example.
/*
* const stateShape = {
* entities: {
* [pluralEntityName]: {
* [entityId]: entityObject,
* },
* },
* };
*/
const exampleState = {
entities: {
users: {
'123': { name: 'John', id: 123, hobby: 'sports' },
},
},
};
And here is an example of selecting entities from the redux
store above.
import { createSelector } from 'reselect';
const entitiesSelector = createSelector(
// Return entities based on type
// Will be recomputed if state, type, or name change.
(state, type, /* name */) => state.entities[type],
// Return the name exactly as passed in
// Will be recomputed if state, type, or name change.
(state, type, name) => name,
// Expensive calculation here!!!
// Return something computed from the other 2 return values.
// Outputs of the other functions are used as inputs to the last function.
// Will be recomputed if entities or name change.
(entities, name) =>
Object.values(entities)
.filter((entity) => entity.name === name),
);
// Example usage:
const mapStateToProps = (state) => ({
allUsersNamedBob: entitiesSelector(state, 'users', name: 'Bob'),
});
If you are experienced with reselect
you will see the problems right away. You may be thinking, “I hate contrived examples in tech articles”. Unfortunately this is not very contrived and has been far too real for far too long 😭.
If you are not experienced with reselect
don’t worry, read on!
In reselect
’s createSelector the final function takes the results of all the previous functions as arguments. Each of the “previous functions” takes the arguments the selector is called with. There is an alternative way to write the functions to draw a clear distinction between the two types of function. Putting the first set of functions in array identifies all the intermediate selectors.
const entitiesSelector = createSelector(
[
(state, type, /* name */) => state.entities[type],
(state, type, name) => name,
],
(entities, name) =>
Object.values(entities)
.filter((entity) => entity.name === name),
);
Great! We can select and filter arrays of entities by name everywhere in our application! Wow isn’t this convenient, and we wrote very little code! But what is the “magic” that reselect
is doing? The benefit of reselect
is memoization. Memoization means storing the results of function calls to use them in place of recomputing the value.
A functional programming aside: A pure function gives the same output for any set of inputs and doesn’t have any side effects. This means if you call the function twice with the same arguments you can use the result of the first call and forget about the second call all together. A perfect candidate for memoization! And… selectors are pure functions (a side-effect of this is they are super testable too)!
reselect
selectors keep track of the arguments and return values and only recalculate the return value if the arguments have changed. I said “magic” above but reselect
is around 75 lines of code and there is not much magic going on. It is a library that is definitely worth reading if you use it!
Unfortunately there is a classic gotcha in the example above. entitiesSelector has a cache size of 1 so using the selector in more than one location (or more than one instance of a component) results in recalculating often.
const mapStateToProps = (state) => ({
bobUsers: entitiesSelector(state, 'users', 'Bob'),
karenUsers: entitiesSelector(state, 'users', 'Karen'),
});
The example above will break memoization because of the change in the name argument. In fact, even cross component usage in two separate mapStateToProps functions will break memoization!
One solution is to build a factory for each option.
const allUsersNamedFactory = (name) => createSelector(
(state) => entitiesSelector(state, {
type: 'users',
name,
}),
(users) => users,
});
// We pull this out of the mapStateToProps body because otherwise a new
// selector is created on every call of mapStateToProps, this also renders the
// selector's memoization useless.
const karenSelector = allUsersNamedFactory('Karen');
const bobSelector = allUsersNamedFactory('Bob');
// Example usage:
const mapStateToProps = (state) => ({
bobUsers: bobSelector(state),
karenUsers: karenSelector(state),
});
This is perfect! We are back in business. These 2 selectors can be used all over the application with expected memoization. The drawbacks of this are:
These selectors are no longer configurable; we can’t dynamically select based on props in mapStateToProps
There needs to be a selector created for every combination of argument values that is used
Another solution (that maintains dynamic selecting ability based on props) is to make an entitySelector factory and then use makeMapStateToProps instead of mapStateToProps. This will call the factory per component instance giving you a fresh instance of entitySelector:
// First we need to change entitySelector into a factory
const makeEntitySelector = () => createSelector(
(state, type, /* name */) => state.entities[type],
(state, type, name) => name,
(entities, name) =>
Object.values(entities)
.filter((entity) => entity.name === name),
);
// Example usage:
const makeMapStateToProps = () => {
// A new entitySelector is created for each component instance.
const entitySelector = makeEntitySelector();
const mapStateToProps = (state, props) => ({
users: entitySelector(state, 'users', props.name),
});
return mapStateToProps;
}
Hopefully this illustrates there is a lot of boilerplate and thought that goes into correct selector usage. But luckily you are already using redux
so you love boilerplate. One minor drawback to boilerplate is that it is easy to forget, or mess up. In the case of selectors this most likely results in your application silently using selectors incorrectly. Hmm, silent failure. That sounds pretty bad.
This is where re-reselect
comes in. re-reselect
builds a cache of multiple reselect
selectors cached based on input arguments. So our entitiesSelector would now be:
import createCachedSelector from 're-reselect';
const entitiesSelector = createCachedSelector(
(state, type, /* name */) => state.entities[type],
(state, type, name) => name,
(entities, name) =>
Object.values(entities)
.filter((entity) => entity.name === name),
)(
(state, type, name) => `${type}:${name}`
);
// Example usage:
const mapStateToProps = (state) => ({
bobUsers: entitiesSelector(state, 'users', 'Bob'),
karenUsers: entitiesSelector(state, 'users', 'Karen'),
});
The only difference is now we include a function to calculate the cache key based on selector input arguments. The usage is the same as reselect
but with proper memoization! This means re-reselect
can be a drop in replacement even if you are using reselect
incorrectly. re-reselect
uses the cache key to create/get a different reselect
selector based on different arguments.
At Riipen, we were using reselect
incorrectly for a long time. The selectors were a part of the code base that nobody wanted to touch. It seemed like they had a high degree of complexity and a huge impact. Almost every single component uses selectors and a refactor would have a huge impact with no current perceivable benefit. We eventually bit the bullet on this piece of technical debt and invested approximately a week of developer time into adding unit tests for our selectors and refactoring. The benefit has been a huge confidence boost in the teams dealings with selectors and no mysterious performance degradation in the future. Hopefully our mistakes can help inform your decisions on how to properly integrate reselect
and re-reselect
into your React
/redux
project.