ReduxJS/Toolkit: createSelector

Batteries Included: Reselect

ReduxJS/Toolkit: createSelector

posted in javascript on  •   • 

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 selectors
  • combiner: The second argument, the result function, calculates the derived state
  • createSelectorOptions: 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.

How reselect works

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.


Stuff that came into being during the making of this post
Tags: tutorial react