03 May 2021
Reselect: the redux memoizer we need, but not the redux memoizer we deserve
The way Redux and React fit together didn’t really click for me until I finally started using the Reselect library.
The standard model that is presented for using Redux with React is:
- You have your data in a store (your state)
- You have a React component that can render data
- You pass the data from the store to the React component using the react-redux
connect
function with amapStateToProps
function that transforms the state into props for your component - The
connect
function is called whenever anything changes in the state
If you use the React Developer Tools Chrome Extension you can even click into Components > Settings and set it to “Highlight updates when components render”, which will highlight components on the page as they re-render:
You may notice on some state updates that parts of the page are re-rendering that have nothing to do with the changed state. A React component rendered via the connect
function will get re-rendered along with all of its children whenever any of its props change via strict equality comparison. (This is very much like the re-rendering logic for PureComponents.)
Strict equality comparison is done with ===
(AKA the portmanteau “threequals”). For any primitive data types—e.g., number, string, boolean, null, undefined—two variables representing the same value will match (7 === 7
and true === true
). However, for reference types—e.g., Object, Array, Function—two variables need to point to the same reference in memory to be strictly equal ([] !== []
and {foo: 'bar'} !== {foo: 'bar'}
).
If we look at a lot of mapStateToProps
functions, this can cause a big problem, because often they are generating new objects each time they are called and these objects aren’t strictly equal to what was returned the previous time.
One such pattern I’ve used before is to split up a sortable list of complex objects into a mapping of id to object and an array of the ordered ids. In this case they were Layers
, so the state looked something like this:
{
"idToLayer": {
"1": { "name": "Layer 1" },
"2": { "name": "Layer 2" }
},
"orderedLayerIds": [2, 1]
}
My React component expected a single array of Layers, so in the mapStateToProps
function I had this code:
const mapStateToProps = (state) => ({
layers: state.orderedLayerIds.map(id => state.idToLayer[id])
});
This pattern felt great. As opposed to just having all my Layer objects smooshed together in an array, it allowed me to make edits to any layer without having to do it within an array and it allowed me to order the layers separate from the objects themselves.
Unfortunately, I was seeing that this component would re-render with every single state update, even if it was unrelated changes. This was happening because my mapStateToProps
function was creating a new layers
object every time it was called, and since the connect
function is calling this every time the state changes anywhere in my app, it was always sending a new layers object to my component.
The solution here was Reselect. It is billed as a memoization library. Memoization is described on Wikipedia as “an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again”. This seemed like a memory bloat nightmare, but upon reading the source code I realized that a memoized selector caches only the most recent computation and recomputes whenever its inputs change. The purpose in this case isn’t so much to prevent regenerating the props from the state, but more importantly to prevent unnecessary re-renders by returning strictly equal objects when their underly values haven’t changed.
So, by using a memoized selector with reselect we essentially do:
state -> inputs to compare -> cached result OR generate new result
or from our layers example before, here’s what a selector would look like:
import { createSelector } from 'reselect';
export const layersSelector = createSelector(
[
(state) => state.idToLayer,
(state) => state.orderedLayerIds,
],
(idToLayer, orderedLayerIds) =>
orderedLayerIds.map((id) => idToLayer[id])
);
The first argument to createSelector
is an array of functions that take the state and return a value. These values will be compared for strict equality, so you should make sure to never generate anything new in here.
The second argument is the function that will get called if the inputs don’t match. It receives all those inputs as arguments and then it’s able to generate the desired object—in this case the layers array.
With a simple update to our mapStateToProps
function, we’re off the races!
const mapStateToProps = (state) => ({
layers: layersSelector(state)
});
Now our layers
prop will only update if there was an actual update to our layers or their ordering. A very simple library, but powerful results!
Also, if you made it this far, then you may have realized my intro graphic was rather confusing, since this had nothing to do with selecting text twice.