Understanding reselect and re-reselect

TL; DR

If you’re pretty comfortable with what Redux selection is and how it works, remember these tips:

Keep simple selectors simple

For simple use cases where we want to do a straight property lookup, all we need to do in mapStateToProps is drill down to the property we need. For example:

const mapStateToProps = (state, ownProps) => ({
    locationChanging: state.grid.locationChanging,
})

Property lookups on objects are fast. They allow us to select the data we care about, and they don’t require any further optimization.

If you have a unique ID, you probably should use re-reselect

When you have access to a unique ID that you’re using to calculate some data, or display a UI component in a list of items by ID, you would probably benefit from using re-reselect. This is because you have an easy way to get a dependable cache key that refers to the same entity over time.

For more details on why this is true, see the reselect docs.

You don’t have to use a memoized selector with redux-saga’s select

When writing sagas with redux-saga that interact with the Redux store, you will commonly use the select effect. It performs the same task as any other selector: targeting a specific piece of the Redux state that we want to get updates on. The select effect accepts a selector function and any number of optional (...args) (see the docs. Essentially the same ideas apply here as with mapStateToProps, except that the selector does not necessarily get ownProps as its second argument.

If you are doing a straight property lookup, you do not need to use a memoized selector. You can pass a regular function as the selector argument:

const isGridReady = yield select(state => state.grid.isGridReady)

In many cases, you don’t need anything more complex than this.


Defining ‘selection’

The core issue we’re dealing with is how to drill down to data that we need to get from the Redux state tree. Selection in this context is the act of targeting a specific piece of the Redux state that we want to get updates on. If our selector works correctly, the component that uses it will update when – and only when – there is a state update that it needs.

Connecting to the store with mapStateToProps

The connect function provided by react-redux gives us access to the Redux store with its first argument, mapStateToProps. mapStateToProps is a function that takes in two arguments, state and ownProps. state is the current state of the Redux store. ownProps are the props that are passed in to the instance of the component. mapStateToProps returns an object of props that is merged with the passed-in props to provide the final props object to the component.

Computing derived data with selectors

There are some cases when we need to do some computation to derive the data we need, based on what is in state and ownProps.

Consider this example:

const ChosenItemsList = props => {
    // ... more component code
};

const mapStateToProps = (state, ownProps) => {

    // `state.chosenItems` is a list of items that have been clicked on by the user.
    const { chosenItems } = state.chosenItems;

    // `itemType` is a given description, such as 'image' or 'zipFile'
    const { itemType } = ownProps;

    const chosenItemsByType =
        chosenItems.filter(chosenItem => chosenItem.type === itemType);

    return { chosenItemsByType };
};

export default connect(mapStateToProps)(ChosenItemsList);

The selection logic requires us to iterate through the state.chosenItems array. This is a potentially expensive (linear time complexity) operation when state.chosenItems contains a lot of elements.

Memoizing selection logic

Rather than putting that expensive array.filter() in mapStateToProps, we can use a selector to memoize the operation. Reselect gives us the createSelector function that does exactly this.

createSelector takes any number of function arguments. The first to n-1 functions are used as “input selectors”, and the nth function is turned into a memoized selector.
– input selectors do not transform the data they select; they merely select the inputs for the memoized selector.
– the memoized selector takes any number of selected inputs and computes the derived data.

const ChosenItemsList = props => {
    // ... more component code
};

const chosenItemsSelector = state => state.chosenItems;
const itemTypeSelector = (state, ownProps) => ownProps.itemType;

const chosenItemsByTypeSelector = createSelector(
    chosenItemsSelector,
    itemTypeSelector,
    (chosenItems, itemType) => {
        // This function will be memoized; it will only be called when the arguments change.
        // If the arguments have not changed, the result from the last time it was called will
        // be returned.
        const chosenItemsByType =
            chosenItems.filter(chosenItem => chosenItem.type === itemType);

        return { chosenItemsByType };
    });

const mapStateToProps = (state, ownProps) => ({
    chosenItemsByType: chosenItemsByTypeSelector(state, ownProps),
});

export default connect(mapStateToProps)(ChosenItemsList);

We’re able to avoid that expensive array iteration if our selector is used when neither chosenItems nor itemType have changed since the last time it was called.

Warnings about reselect

=== diffs

Reselect checks for changes using reference equality (===). This means that you have to respect the Redux expectation that your store’s properties are immutable. If your reducers modify some data within the state.items array without returning a new object reference, the selector will not detect a new argument, and you will receive a cached item when you expect to receive the newest one.

Cache size of 1

Per the reselect docs:

Selectors created with createSelector have a cache size of 1. This means they always recalculate when the value of an input-selector changes, as a selector only stores the preceding value of each input-selector.

This means that your expensive calculations will always run when an input changes. This applies across all call sites for a given selector. Let’s assume you have two instances of <ChosenItemsList>, each with different itemType props.

<div>
    <MainContent>
        <ChosenItemsList itemType={'image'} />
    </MainContent>
    <Sidebar>
        <ChosenItemsList itemType={'zipFile'} />
    </Sidebar>
</div>

Let’s assume that <MainContent> and <Sidebar> are receiving props updates at different points in time, causing us to alternate between rendering the <ChosenItemsList> components. If this happens, the memoized function will always run, because it is receiving a different itemType argument. This is explained in the reselect docs.

Using re-reselect

How can we avoid this? By changing our caching logic. Re-reselect give us the ability to set up more than one cache for our selector. Its main export is a function called createCachedSelector, which is actually a wrapper around createSelector from reselect. It creates a separate cache for each cache key you create. The first portion of the function signature for createCachedSelector is the same as for createSelector. However, it adds a final argument: a keySelector that returns the cache key.

The keySelector function receives the same arguments as the final memoized selector – in the example below, it gets chosenItems and itemType.

const ChosenItemsList = props => {
    // ... more component code
};

// `state.chosenItems` is a list of items that have been clicked on by the user.
const chosenItemsSelector = state => state.chosenItems;

// `itemType` is a given description, such as 'image' or 'zipFile'
const itemTypeSelector = (state, ownProps) => ownProps.itemType;

const chosenItemsByTypeSelector = createCachedSelector(
    chosenItemsSelector,
    itemTypeSelector,
    (chosenItems, itemType) => {
        // A copy of this selector function will be memoized for each different `itemType`.
        const chosenItemsByType =
            chosenItems.filter(chosenItem => chosenItem.type === itemType);

        return { chosenItemsByType };
    })
    (
        // This is the `keySelector` function. It returns a number or string that is used as the
        // cache key for each copy of the memoized selector function.
        (chosenItems, itemType) => itemType;
    );

const mapStateToProps = (state, ownProps) => ({
    chosenItemsByType: chosenItemsByTypeSelector(state, ownProps),
});

export default connect(mapStateToProps)(ChosenItemsList);

This means that all instances of <ChosenItemsList> that have the same itemType will share the same selector cache. In the situation above where we have two <ChosenItemsList>s with different itemType props, we can alternate between rendering the components and be sure that the memoized selector will only run when chosenItems has changed since the last selector call.

Using re-re-reselect

And if you’ve made it this far, you might just want to check out re-re-reselect. Or not, your choice 🙂

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *