Idiomatic Redux: Using Reselect Selectors for Encapsulation and Performance
An overview of why and how to use Reselect with React and Redux
Note: This post was originally published on my blog at blog.isquaredsoftware.com, and is part of my “Idiomatic Redux” blog series on good Redux usage practices.
In a good Redux architecture, you are encouraged to keep your store state minimal, and derive data from the state as needed. As part of that process, we recommend that you use “selector functions” in your application, and use the Reselect library to help create those selectors. Here’s a deeper look at why this is a good idea, and how to correctly use Reselect.
Basics of Selectors
A “selector function” is simply any function that accepts the Redux store state (or part of the state) as an argument, and returns data that is based on that state. Selectors don’t have to be written using a special library, and it doesn’t matter whether you write them as arrow functions or the
function keyword. For example, these are all selectors:
You can call your selector functions whatever you want, but it’s common to prefix them with
get, or end the name with
fooSelector (see this Twitter poll on naming selectors for discussion).
The first reason to use selector functions is for encapsulation and reusability. Let’s say that one of your
mapState functions looks like this:
That’s a totally legal statement. But, imagine that you’ve got several components that need to access that field. What happens if you need to make a change to where that piece of state lives? You would now have to go change every
mapState function that references that value. So, in the same way that we recommend using action creators to encapsulate details of creating actions, we recommend using selectors to encapsulate the knowledge of where a given piece of state lives. Ideally, only your reducer functions and selectors should know the exact state structure, so if you change where some state lives, you would only need to update those two pieces of logic.
One common description of selectors is that they’re like “queries into your state”. You don’t care about exactly how the query came up with the data you needed, just that you asked for the data and got back a result.
Reselect Usage and Memoization
The next reason to use selectors is to improve performance. Performance optimization generally involves doing work faster, or finding ways to do less work. For a React-Redux app, selectors can help us do less work in a couple different ways.
Let’s imagine that we have a component that requires a very expensive filtering/sorting/transformation step for the data it needs. To start with, its
mapState function looks like this:
Right now, that expensive logic will re-run for every dispatched action that results in a state update, even if the store state that was changed was in a part of the state tree that this component doesn’t care about.
What we really want is to only re-run these expensive steps if
state.someData has actually changed. This is where the idea of "memoization" comes in.
Memoization is a form of caching. It involves tracking inputs to a function, and storing the inputs and the results for later reference. If a function is called with the same inputs as before, the function can skip doing the actual work, and return the same result it generated the last time it received those input values.
The Reselect library provides a way to create memoized selector functions. Reselect’s
createSelector function accepts one or more "input selector" functions, and an "output selector" function, and returns a new selector function for you to use.
createSelector can accept multiple input selectors, which can be provided as separate arguments or as an array. The results from all the input selectors are provided as separate arguments to the output selector:
When you call the selector, Reselect will run your input selectors with all of the arguments you gave, and looks at the returned values. If any of the results are
=== different than before, it will re-run the output selector, and pass in those results as the arguments. If all of the results are the same as the last time, it will skip re-running the output selector, and just return the cached final result from before.
In typical Reselect usage, you write your top-level “input selectors” as plain functions, and use
createSelector to create memoized selectors that look up nested values:
Note that the second time we called
selectResult, the "output selector" didn't execute. Because the results of
selectB were the same as the first call,
selectResult was able to return the memoized result from the first call.
It’s important to note that by default, Reselect only memoizes the most recent set of parameters. That means that if you call a selector repeatedly with different inputs, it will still return a result, but it will have to keep re-running the output selector to produce the result:
Also, you can pass multiple arguments into a selector. Reselect will call all of the input selectors with those exact inputs:
Because of this, it’s important that all of the “input selectors” you provide should accept the same types of parameters. Otherwise, the selectors will break.
In this example,
selectItemId expects that its second argument will be some simple value, while
selectOtherField expects that the second argument is an object. If you call
selectOtherField will break because it's trying to access
You can (and probably should) use selector functions anywhere in your application that you access the state tree. That includes
mapState functions, thunks, sagas, observables, middleware, and even reducers.
Selector functions are frequently co-located with reducers, since they both know about the state shape. However, it’s up to you where you put your selector functions and how you organize them.
Optimizing Performance With Reselect
Let’s go back to the “expensive
mapState" example from earlier. We really want to only execute that expensive logic when
state.someData has changed. Putting the logic inside a memoized selector will do that.
This is a big performance improvement, for two reasons.
First, now the expensive transformation only occurs if
state.someData is different. That means if we dispatch an action that updates
state.somethingElse, we won't do any real work in this
Second, the React-Redux
connect function determines if your real component should re-render based on the contents of the objects you return from
mapState, using "shallow equality" comparisons. If any of the fields returned are
=== different than the last time, then
connect will re-render your component. That means that you should avoid creating new references in a
mapState function unless needed. Array functions like
filter() always return new array references, and so does the object spread operator. By using memoized selectors, we can return the same references if the data hasn't changed, and thus skip re-rendering the real component.
Advanced Optimizations with React-Redux
There’s a specific performance issue that can occur when you use memoized selectors with a component that can be rendered multiple times.
Let’s say that we have this component definition:
In this example,
SomeComponent is passing
ownProps.itemId as a parameter to the selector. When we render multiple instances of
<SomeComponent>, each of those instances are sharing the same instance of the
selectItemForThisComponent function. That means that when an action is dispatched, each separate instance of
<SomeComponent> will separately call the function, like:
As described earlier, Reselect only memoizes on the most recent inputs (ie, it has a cache size of 1). That means that
selectItemForThisComponent will never memoize correctly, because it's never being called with the same inputs back-to-back.
This code will still run and work, but it’s not fully optimized. For the absolute best performance, we need a separate copy of
selectItemForThisComponent for each instance of
connect function supports a special "factory function" syntax for
mapDispatch functions, which can be used to create unique instances of selector functions for each component instance.
If the first call to a
mapDispatch function returns a function instead of an object,
connect will use that returned function as the real
mapDispatch function. This gives you the ability to create component-instance-specific selectors inside the closure:
Both component 1 and component 2 will get their own unique copies of
selectItemForThisComponent, and each copy will get called with consistently repeatable inputs, allowing proper memoization.
Like other common Redux usage patterns, you are not required to use selector functions in a Redux app. If you want to write deeply nested state lookups directly in your
mapState functions or thunks, you can. Similarly, you don't have to use the Reselect library to create selectors - you can just write plain functions if you want.
Having said that, you are encouraged to use selector functions, and to use the Reselect library for memoized selectors. There’s also many other options for creating selectors, including using functional programming utility libraries like lodash/fp and Ramda, and other alternatives to Reselect. There’s also utility libraries that build on Reselect to handle specific use cases.
Selector Basics and Usage
- ReactCasts #8: Selectors in Redux
- Optimizing React Redux Application Development with Reselect
- Usage of Reselect in a React-Redux Application
- React, Reselect, and Redux
- React/Redux Links: Redux Reducers and Selectors
- Redux Ecosystem Links: Utilities — Selectors
- Dan Abramov’s “Idiomatic Redux” video series: Colocating Selectors with Reducers
- Randy Coulman’s posts on selectors and encapsulation: Encapsulating the Redux State Tree , Redux Reducer/Selector Asymmetry, Modular Reducers and Selectors , and Globalizing Redux selectors
- React/Redux Links: Redux Architecture — Encapsulation and Reusability
Originally published at blog.isquaredsoftware.com