Reputation: 11543
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
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