ReduxJS/Toolkit: createSelector
posted in javascript on • by Wouter Van Schandevijl •
reduxjs/reselect
:
Create memoized ‘selector’ functions
One of the Redux Style Guide “Strongly Recommended” rules is Keep State Minimal and Derive Additional Values:
Keep the actual data in the Redux store as minimal as possible, and derive additional values from that state as needed.
Keeping all that efficient is where createSelector
comes into play.
Examples
By convention, selectors are named selectThing
:
inputSelectors
: The first argument, the dependencies, is an array of root state selectorscombiner
: The second argument, the result function, calculates the derived statecreateSelectorOptions
: The third (optional) options argument configures how the selector behaves
import { createSelector } from "@reduxjs/toolkit";
const selectMyTodosCompletedCount = createSelector(
[
(state: RootState) => state.todos,
(state: RootState) => state.user.id,
],
(todos, userId) => {
return todos.filter(todo => todo.userId === userId && todo.done).length;
}
)
// Usage in a component
import { useSelector } from "react-redux";
const TodosDoneCount = () => {
const doneCount = useSelector(selectMyTodosCompletedCount);
return <>✔️ {doneCount}</>;
}
Every time the root state changes, it will execute the inputSelectors; in this case
state.todos
and state.user.id
. If the returned references have not changed, the memoized
count is returned, otherwise the combiner is executed.
This is also why you want to select the todos and the userId separately, if you select them
as ({todos: state.todos, userId: state.user.id})
, you’d get a new reference every time,
effectively rendering the selector useless!
The Reselect development-only inputStabilityCheck
executes the selector twice and logs a warning to the console
if one of the inputSelectors returns a different reference.
With Parameters
Filter the todo list with a needle parameter.
const selectTodos = createSelector(
[
(state: RootState) => state.todos,
(_, needle: string) => needle,
],
(todos, needle) => todos.filter(todo => todo.text.includes(needle))
)
// Usage in a component
const TodoList = ({searchNeedle}) => {
const todos = useSelector(state => selectTodos(state, searchNeedle));
return todos.map(todo => (
<Todo key={todo.id} todo={todo} />
);
}
If you don’t like this syntax, see the Reselect FAQ on how to create a curried selector instead.
// This would allow you to do this instead:
const todos = useSelector(selectTodos(searchNeedle));
// The FAQ also contains a recipe to turn this into:
const todos = useSelectTodos(searchNeedle);
Multiple Parameters
const selectTodos = createSelector(
[
(state: RootState) => state.todos,
(_, needle: string) => needle,
(_, __, caseSensitive: boolean) => caseSensitive
],
(todos, needle, caseSensitive) => { /* magic here */ }
)
createSelectorOptions
Reselect will only execute the combiner fn when the inputs have changed, which is typically your entire root state (with potentially additional parameters) AND the result of those input selectors has changed.
Configure this behavior with argsMemoize(Options)
and memoize(Options)
.
If you want to change the default behavior of many/all selectors, use createSelectorCreator.
lruMemoize
lruMemoize
(Least Recently Used) was the default before v5 of Reselect.
It had a default cache size of 1, meaning a recomputation every time
one of the inputSelectors changed.
The “problem” with lruMemoize was that a cache of size 1 might
not be that useful! This was typically solved with useMemo
or by passing memoizeOptions: { maxSize: 5 }
.
import { lruMemoize } from "reselect";
import { shallowEqual } from "react-redux";
export const selectMyTodosCompletedCount = createSelector(
[(state: RootState) => ({todos: state.todos, userId: state.more.users.ids[0]})],
({todos, userId}) => todos.filter(todo => todo.userId === userId && todo.done).length,
{
memoize: lruMemoize,
memoizeOptions: {
equalityCheck: (a, b) => a.todos === b.todos && a.userId === b.userId,
// resultEqualityCheck: shallowEqual,
// maxSize: 10
},
// argsMemoize: lruMemoize,
// argsMemoizeOptions: {
// equalityCheck: shallowEqual,
// resultEqualityCheck: shallowEqual,
// maxSize: 10
// }
}
)
weakMapMemoize (default)
weakMapMemoize
is the new default since v5 and provides a dynamic cache size
out of the box.
unstable_autotrackMemoize (experimental)
There is also the experimental unstable_autotrackMemoize
,
which, like proxy-memoize
below, can be more efficient.
import { unstable_autotrackMemoize } from "reselect";
// The combiner (todos.filter) will NOT execute when we
// toggle a Todo between done/not done as that field is
// not accessed.
const selectTodos = createSelector(
[
(state: RootState) => state.todos,
(_, needle: string) => needle,
],
(todos, needle) => todos.filter(todo => todo.text.includes(needle)),
{
memoize: unstable_autotrackMemoize,
}
)
createSlice
Common selectors can already be defined in the createSlice
setup.
const todoSlice = createSlice({
initialState: [],
// Predefined selectors, which receive the slice's state as their first parameter.
selectors: {
selectTodo: (state, id: number): Todo | undefined => {
return state.find(todo => todo.id === id);
},
},
});
export const { selectTodo } = todoSlice.selectors;
// Usage in a component
const todo = useSelector(state => selectTodo(state, todoId));
TypeScript
See ReduxJS/Toolkit: TypeScript
if you’re unsure how to create the RootState
.
import { createSelector } from "@reduxjs/toolkit";
const createAppSelector = createSelector.withTypes<RootState>();
Debugging
skortchmark9/reselect-tools
:
Debugging Tools for Reselect
Once you get many selectors depending on other selectors, you can use this tool to visualize re-computations.
If your debugging needs are basic, you can check the Output Selector Fields:
const meta = {
lastResult: selectMyTodosCompletedCount.lastResult(),
recomputations: selectMyTodosCompletedCount.recomputations(),
dependencyRecomputations: selectMyTodosCompletedCount.dependencyRecomputations(),
}
Alternatives
proxy-memoize
dai-shi/proxy-memoize
:
Intuitive magical memoization library with Proxy and WeakMap
This will re-execute when the done
property toggles even though
it has no impact on the actual result.
import { createSelector } from "reselect";
const selectTodoDescriptionsReselect = createSelector(
[state => state.todos],
todos => todos.map(todo => todo.text)
)
With proxy-memoize
, this would not be the case as due to the proxy,
it figures only text is actually relevant for selector.
import { memoize } from "proxy-memoize";
const selectTodoDescriptionsProxy = memoize(state =>
state.todos.map(todo => todo.text)
)
For this reason the ReduxJS/Toolkit officially encourages consideringusing proxy-memoize as a viable alternative to Reselect.
re-reselect
toomuchdesign/re-reselect
:
Enhance Reselect selectors with deeper memoization and cache management.
As of Reselect v5, what you’d need this library for can now achieved with Reselect createSelectorOptions
natively.