Why reselect is so good
Reselect is a popular library that provides a convenient way of getting values from the store in a React-Redux application. What makes it so good is its memoization ability. You can read all this in the documentation. In two words, when you use the createSelector() function, it memoizes an output of every input selector and recalculates the resulting value only if any of the input selectors changes its output. An important thing to note here is that reselect uses reference equality (===) to determine a value change.
As a motivation to use memoization the documentation suggests an increase of performance, because recalculation on every call may be quite expensive. But we will see in this article that using a memoized selector is sometimes the only way to go in a React-Redux application, even if calculations are very cheap and don’t affect performance.
How a React-Redux connected component works
First of all, let’s take a look at how a React-Redux application works. What Redux does in essence, is provide us with the store for our app’s state and with ways to communicate with the store. One of these ways is the connect() function. After calling connect() on a custom component you get a wrapper that passes state from the store as props to your component. This happens by means of mapStateToProps() function which is called on every state change.
After mapStateToProps() yields recalculated props, the new props are shallow compared to the old ones and if they differ, component gets re-rendered. Again, reference equality (===) is used to compare the props.
An unmemoized selector can ruin your day
Here we can make a use of an example. Let’s make up an application called List of Goods. The app will be based on react-boilerplate with an immutable state (see Immutable.js).
We define a state for our app:
import {SET_GOODS, SET_SORTED, COUNT} from 'constants/index';
import {fromJS} from 'immutable';
const initialState = fromJS({
goods: [
{
name: 'tomatoes',
price: 3,
},
{
name: 'potatoes',
price: 2,
},
{
name: 'cucumbers',
price: 5,
},
{
name: 'salad',
price: 1,
}
],
sorted: false,
});
export default (state = initialState, action) => {
switch (action.type) {
case SET_SORTED: {
return state.set('sorted', action.sorted);
}
default: {
return initialState;
}
}
}
And a couple of components:
class GoodsList extends React.Component {
render () {
return (
<div>
<ul>
{this.props.goods.map((g, i) =>
<li key={i}>{`${g.get('name')} - ${g.get('price')}$`}</li>)}
</ul>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
goods: getGoods(state),
};
}
const mapDispatchToProps = (dispatch) => bindActionCreators({
count,
}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(GoodsList);
class Buttons extends React.Component {
render () {
return (
<div style={{display: 'flex'}}>
<button
style={buttonStyle}
onClick={() => this.props.setSorted(true)}>
Show Sorted
</button>
<button
style={buttonStyle}
onClick={() => this.props.setSorted(false)}>
Show Unsorted
</button>
</div>
)
}
}
const mapStateToProps = (state) => {
return {}
}
const mapDispatchToProps = (dispatch) => bindActionCreators({
setSorted,
}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(Buttons);
Also, we have a page containing our components:
export default class HomePage extends React.PureComponent {
render() {
return (
<div>
<GoodsList/>
<Buttons/>
</div>
);
}
}
The app only renders the list of goods, either sorted by price (when a user clicks the ‘Show sorted’ button) or unsorted (by default and when a user clicks the ‘Show unsorted’ button). In the state, we have goods which are the very list of goods we want to show and sorted which tells us if the list should be rendered sorted or unsorted.
The only thing we need to do to get it work now is to define a selector. Let's try an unmemoized selector first. Basically, this is just a function, like this:
export const getGoods = (state) => {
const list = state.getIn(['main', 'goods']);
const sorted = state.getIn(['main', 'sorted']);
return sorted ? list.sort((a, b) => {
const aPrice = a.get('price');
const bPrice = b.get('price');
if (aPrice < bPrice) { return -1; }
if (aPrice > bPrice) { return 1; }
if (aPrice === bPrice) { return 0; }
}) : list;
}
Depending on the value of sorted, this selector either just returns a list of goods from the state or sorts it by price before returning. Let’s take a closer look at what happens here. The Immutable.js documentation says: “sort() always returns a new instance, even if the original was already sorted”. This means, our sorted list of goods will never be equal to the previously sorted list of goods. Reference equality for goods prop of GoodsList component will never hold, meaning the component will be re-rendered on every state change, even if this change doesn’t affect the list of goods in any way.
While this is obviously a wrong way to create a selector, it doesn’t look like a big deal so far. The calculations in the selector are very cheap for our list of only four items, there will not be any significant drop of performance. But what if we need to change state in component’s lifecycle? As React documentation suggests, we will do it in componentWillReceiveProps method. Say, we need to count for some reason how many times GoodsList received props. So, we add componentWillReceiveProps to GoodsList component:
componentWillReceiveProps = (nextProps) => {
this.props.count();
}
Here count is an action dispatched to store.
And the reducer will look like this:
import {SET_GOODS, SET_SORTED, COUNT} from 'constants/index';
import {fromJS} from 'immutable';
const initialState = fromJS({
goods: [...],
sorted: false,
count: 0,
});
export default (state = initialState, action) => {
switch (action.type) {
case SET_SORTED: {
return state.set('sorted', action.sorted);
}
case COUNT: {
return state.set('count', state.get('count') + 1);
}
default: {
return initialState;
}
}
}
The following sequence of actions is fired when a user clicks ‘Show sorted’:
- SET_SORTED action with value true is dispatch to store.
- The value of sorted is changed in the state.
- GoodsList connected component calls mapStateToProps.
- Selector getGoods is called from mapStateToProps.
- Selector returns the sorted list which is not equal to the list already passed to GoodsList.
- As mapStateToProps returned props that are not shallow equal to the component’s previous props, React starts GoodsList rerendering.
- GoodsList lifecycle method componentWillReceiveProps is called.
- Action COUNT is dispatch to store.
- The value of count is changed in the state.
- Points 3-9 are repeated once again.
- And then one more time.
- And then… well, it will never stop. We’ve got an endless cycle.
This case with counting received props might look a bit artificial, but this may happen in real life. For example, when you set initial values for a redux-form, an action is dispatched to store. Or if you need to store route params in the state, you will set them as an object which will cause the state to change and you are likely to do that in componentWillReceiveProps(), because a route may change without unmounting a component. Both these cases effectively behave as if you count received props in componentWillReceiveProps().
Reselect may or may not help you
We can easily fix this with Reselect. This selector will work fine in our case:
import {createSelector} from 'reselect';
const getList = (state) => state.getIn(['main', 'goods']);
const getSorted = (state) => state.getIn(['main', 'sorted']);
export const getGoods = createSelector(
getList,
getSorted,
(list, sorted) => {
return sorted ? list.sort((a, b) => {
const aPrice = a.get('price');
const bPrice = b.get('price');
if (aPrice < bPrice) { return -1; }
if (aPrice > bPrice) { return 1; }
if (aPrice === bPrice) { return 0; }
}) : list;
}
)
Here the transform function will not be called until getList or getSorted change their values which they don’t on COUNT action. Instead, getGoods selector just returns the previously calculated value which is obviously equal to the list already passed GoodsList component. React doesn’t try to re-render the component for the second time, the cycle breaks.
There is one peril, however: it is quite easy to accidentally make a Reselect selector unmemoized.
For example, you might want to use a javascript array in your component instead of an immutable list for some reason. But this selector will again cause an endless cycle to start on ‘Show sorted’ click:
import {createSelector} from 'reselect';
const getList = (state) => state.getIn(['main', 'goods']).toJS();
const getSorted = (state) => state.getIn(['main', 'sorted']);
export const getGoods = createSelector(
getList,
getSorted,
(list, sorted) => {
return sorted ? list.sort((a, b) => a.price - b.price) : list;
}
)
Although getGoods is a memoized selector here, it gets a different input from getList every time. In general, selectors that get values from the state shouldn’t do anything else, because they are not memoized.
Another possibility for a mistake is carried, selectors. Sometimes you want to create selectors like this just in case if you need to pass arguments to the selector in future:
export const getGoods = () => createSelector(...)
And it is tempting to write mapStateToProps this way then:
const mapStateToProps = (state) => {
return {
goods: getGoods()(state),
};
}
But here we create a new instance of the selector every time mapStateToProps is called. Basically, we just throw away the memoization ability of our selector, because every new instance calculates its value again and this value is not equal to the value calculated by another instance.
These kinds of the bug are quite annoying because often you don’t notice when you create a bug and stumble across it many commits later. Thankfully, git bisect can help you find where it went wrong.
Top tips to make your life better
To sum up, I will designate some tips I derived for myself while using Reselect library.
-
Always use createSelector() from Reselect to create a selector, if it transforms value from the state in any way.
-
Avoid curried selectors. As a rule, you don’t need them. You can pass all the arguments you need using the second argument of a selector. In fact, the only case I can think of when you need a curried selector is described in the documentation.
-
Be careful with dispatching actions to the store from lifecycle functions. In general, avoid doing this. But if you have to, maybe it is a good idea to compare props manually before dispatching an action.
-
Don’t use selectors like
state => state
or
state => state.get('main')
as input selectors. If you access big parts of the state, these parts are very likely to change on every action. If you really need to, you will probably have to memoize the transform function yourself. This may be done by using memoize() from Lodash or Ramda or something else.
Also, you can find the example I used here on GitHub, if you want to play around with the selectors and simulate some other cases.
More tutorials in this series:
Missing Part of Redux Saga Experience
Using Normalizr to Organize Data in Stores – Practical Guide