Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Reselect Selectors: How to Avoid an Endless Cycle in a React-Redux Application

DZone's Guide to

Reselect Selectors: How to Avoid an Endless Cycle in a React-Redux Application

Endless loops are never fun. Learn how to avoid this particular rabbit hole while developing a web app with React and Redux.

· Web Dev Zone
Free Resource

Tips, tricks and tools for creating your own data-driven app, brought to you in partnership with Qlik.

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 the component gets rerendered. Again, reference equality (===) is used to compare the props.

An Unmemoized Selector Can Ruin Your Day

Here we can make use of an example. Let’s make up an application called List of Goods. The app will be based on react-boilerplate with the 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 to 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 the 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 the goods prop of the GoodsList component will never hold, meaning the component will be rerendered on every state change, even if this change doesn’t affect the list of goods in any way.

While this is obviously a bad 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, so there will not be any significant drop of performance. But what if we need to change state in the component’s lifecycle? As suggested, we will do it in the componentWillReceiveProps method. Say we need to count, for some reason, how many times GoodsList received props. So, we add componentWillReceiveProps to the 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’:

  1. SET_SORTED action with value true is dispatched to store.
  2. The value of sorted is changed in the state.
  3. GoodsList connected component calls mapStateToProps.
  4. Selector getGoods is called from mapStateToProps.
  5. Selector returns the sorted list which is not equal to the list already passed to GoodsList.
  6. As mapStateToProps returned props that are not shallow equal to the component’s previous props, React starts GoodsList rerendering.
  7. GoodsList lifecycle method componentWillReceiveProps is called.
  8. Action COUNT is dispatched to store.
  9. The value of count is changed in the state.
  10. Points 3-9 are repeated once again.
  11. And then one more time.
  12. 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, the getGoods selector just returns the previously calculated value which is obviously equal to the list already passed to the GoodsList component. React doesn’t try to rerender 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 the immutable list for some reason. But this selector will again cause an endless cycle to start when ‘Show sorted’ is clicked:

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 a selector in the 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 a 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 the Reselect library.

  1. Always use createSelector() from Reselect to create a selector if it transforms value from the state in any way.
  2. 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.
  3. 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.
  4. 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.

Explore data-driven apps with less coding and query writing, brought to you in partnership with Qlik.

Topics:
react ,redux ,react developer ,memoization ,web dev

Published at DZone with permission of Ilya Bohaslauchyk. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}