Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 994
Dávid Molnár Avatar asked Dec 21 '17 13:12

Dávid Molnár


People also ask

How does reselect work?

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.

What is the purpose of memoized selectors provided by the reselect library?

Reselect is a library for building memoized selectors. We define selectors as the functions that retrieve snippets of the Redux state for our React components. Using memoization, we can prevent unnecessary rerenders and recalculations of derived data which in turn will speed up our application.

Why we use reselect in Redux?

Why re-reselect? We can use re-reselect to create memoized selectors. The result will be stored in the memory, and it will be fetched only if the value doesn't change. Each selector has its own memoization, meaning it cannot cache multiple selectors.

What is Redux reselect?

A library for creating memoized "selector" functions. Commonly used with Redux, but usable with any plain JS immutable data as well. Selectors can compute derived data, allowing Redux to store the minimal possible state.


1 Answers

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.

like image 198
markerikson Avatar answered Sep 22 '22 13:09

markerikson