Dávid Molnár
Dávid Molnár

Reputation: 11543

Reselect will not correctly memoize with multiple instances of the same component

I'm reading the documentation for Redux and got stuck with reselect. The code below creates a selector and the documentation says, if we want to use it in two VisibleTodoList components then it won't work correctly.

import { createSelector } from 'reselect'

const getVisibilityFilter = (state, props) => state.todoLists[props.listId].visibilityFilter

const getTodos = (state, props) => state.todoLists[props.listId].todos

const getVisibleTodos = createSelector([getVisibilityFilter, getTodos], (visibilityFilter, todos) => {
  switch (visibilityFilter) {
    case 'SHOW_COMPLETED':
      return todos.filter(todo => todo.completed)
    case 'SHOW_ACTIVE':
      return todos.filter(todo => !todo.completed)
    default:
      return todos
  }
})

export default getVisibleTodos

Using the getVisibleTodos selector with multiple instances of the visibleTodoList container will not correctly memoize

const mapStateToProps = (state, props) => {
  return {
     // WARNING: THE FOLLOWING SELECTOR DOES NOT CORRECTLY MEMOIZE
     todos: getVisibleTodos(state, props)
  }
}

What does this mean? I can not figure out why it wouldn't work.

Upvotes: 6

Views: 1540

Answers (1)

markerikson
markerikson

Reputation: 67459

Correct. That's because Reselect by default only memoizes on the most recent set of inputs:

const a = someSelector(state, 1); // first call, not memoized
const b = someSelector(state, 1); // same inputs, memoized
const c = someSelector(state, 2); // different inputs, not memoized
const d = someSelector(state, 1); // different inputs from last time, not memoized

In those cases, the selector still retrieves data, it just has to recalculate the result even though it saw the inputs at some point in the past.

So, if you are using a selector in a mapState function, and it references a value from ownProps, then multiple instances of the component will likely cause the selector to never memoize properly

const mapState = (state, ownProps) => {
    const item = selectItemForThisComponent(state, ownProps.itemId);

    return {item};
}


// later
<SomeComponent itemId={1} />
<SomeComponent itemId={2} />

In that example, selectItemForThisComponent will always get called with (state, 1) and (state, 2) back-to-back, so it won't memoize right.

One solution is to use the "factory function" syntax supported by connect. If your mapState function returns a function the first time it's called, connect will use that as the real mapState implementation. That way, you can create unique selectors per component instance:

const makeUniqueSelectorInstance = () => createSelector(
    [selectItems, selectItemId],
    (items, itemId) => items[itemId]
);    


const makeMapState = (state) => {
    const selectItemForThisComponent = makeUniqueSelectorInstance();

    return function realMapState(state, ownProps) {
        const item = selectItemForThisComponent(state, ownProps.itemId);

        return {item};
    }
}

export default connect(makeMapState)(SomeComponent);

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.

update

I've expanded on this answer in my blog post Idiomatic Redux: Using Reselect Selectors for Performance and Encapsulation.

Upvotes: 16

Related Questions