If redux
is our front-end database, selectors are reusable functions that let
us query our database. There are some rules that are required for selectors to
be useful:
- Pure functions (deterministic: given the same inputs they will always return the same outputs)
- Function signature:
(state: State, props?: { [key: string]: any }) => any
Selectors ought to keep to the function signature above. We should also try to avoid using any and instead type exactly when the function requires and returns.
1const selectControlsByLineage = (
2 state: State,
3 props: { lineage: string },
4): Control[] => {};
What is reselect? #
reselect
is a third-party library that helps us build composable selectors as
well as dramatically improve the performance of our queries with memoization.
Memoization is a technique to cache the result of a function. Since selectors
must be pure functions, if the inputs are the same, then the output must also be
the same from the previous computation. This is one of the main mechanisms we
use to improve performance of our application. We use selectors on every single
page within our front-end application and some of the queries we make to our
database can be complex. Determining the time complexity of a selector is a
crucial part of improving the performance of our application. By leveraging
reselect, we sacrifice memory for CPU cycles.
I ask everyone reading this to please spend 10 minutes reading the reselect docs. They do a great job explaining the API with plenty of examples on how to use it.
When should I use reselect? #
When to use reselect is very context dependent. Since these are reusable queries, they have the opportunity to save other developers a lot of time building their own selectors. When it comes to using reselect for performance reasons, it’s always recommended to analyze the performance of a query before and after using reselect. Having said that, I use a very simple heuristic for when to use reselect:
If the time complexity for the query is equal to or worse than linear time O(n) then we should probably build the selector using createSelector.
Setup #
1interface ToDo {
2 id: string;
3 text: string;
4 completed: boolean;
5}
6
7interface State {
8 todos: { [key: string]: ToDo };
9}
10
11const state: State = {
12 todos: {
13 1: { id: 1, text: "learn what a selector is", completed: true },
14 2: { id: 2, text: "learn what reselect is", completed: true },
15 3: { id: 3, text: "learn when I should use reselect", completed: false },
16 4: { id: 4, text: "learn how to write selectors", completed: false },
17 },
18};
Example 1 #
1const selectTodos = (state: State) => state.todos;
Should we use reselect for selectTodos? To answer this question we need to understand the time complexity of this function. Accessing a property on an object is O(1) which is faster than linear time. Therefore, we do not need to use reselect.
Example 2 #
1const selectTodoById = (state: State, props: { id: string }) => {
2 const todos = selectTodos(state);
3 return todos[id];
4};
Should we use reselect for selectTodoById
? A hash lookup is O(1), so no we
should not use reselect in this case.
Example 3 #
1const selectCompletedTodos = (state: State) => {
2 const todos = selectTodos(state);
3 return Object.values(todos).filter((todo) => todo.completed);
4};
Should we use reselect for selectCompletedTodos
?
Object.values for v8 appears to be O(n)
and the filter operation on the lists of todos is also O(n). This operation
should be memoized since it requires linear time to complete.
How would we convert the above function to use createSelector
?
1import { createSelector } from "reselect";
2
3const selectTodosAsList = createSelector((todos) => Object.values(todos));
4const selectCompletedTodos = createSelector(
5 selectTodosAsList, // selector composition is critical!
6 (todoList) => todoList.filter((todo) => todo.completed),
7);
createSelector limitation #
It’s important to note that createSelector will only cache the last result. So if the inputs keep changing then it will constantly recompute the query.
Example #
1import { createSelector } from "reselect";
2
3const selectTodosByText = createSelector(
4 selectTodos,
5 (state: State, props: { search: string }) => props.search,
6 (todos, search) => todos.filter((todo) => todo.text.includes(search)),
7);
8
9selectTodosByText(state, { search: "what" });
10// returns a cached result!
11selectTodosByText(state, { search: "what" });
12
13// recomputes because the input changed
14selectTodosByText(state, { search: "when" });
15// recomputes beacuse the input changed again!
16selectTodosByText(state, { search: "what" });
It does not matter if at one point in time we called the selector with the same props, if the last function execution does not match the same inputs as the current function execution then it will recompute the query.
When should I build a selector creator? #
A selector creator is a function that creates selectors. This allows us to get around the last result cache limitation of createSelector that was described previously. A selector creator is particularly useful when we use the same selector in multiple places on the same page.
Example #
1import React from "react";
2import { useSelector } from "react-redux";
3
4const Page = () => {
5 const whenTodos = useSelector((state: State) =>
6 selectTodosByText(state, { search: "when" })
7 );
8 const whereTodos = useSelector((state: State) =>
9 selectTodosByText(state, { search: "where" })
10 );
11
12 return (
13 <div>
14 <div>
15 {whenTodos.map((todo) => <div key={todo.id}>{todo.text}</div>)}
16 </div>
17 <div>
18 {whereTodos.map((todo) => <div key={todo.id}>{todo.text}</div>)}
19 </div>
20 </div>
21 );
22};
In this case, createSelector
is rendered useless because we are constantly
changing the inputs being supplied to our selector selectTodosByText
.
However, if we build a function that creates selectors for us, then we can build as many createSelector for our one query as many times as we want.
1const createSelectorTodosByText = () =>
2 createSelector(
3 selectTodos,
4 (state: State, props: { search: string }) => props.search,
5 (todos, search) => todos.filter((todo) => todo.text.includes(search)),
6 );
7
8import React from "react";
9import { useSelector } from "react-redux";
10
11// do NOT create these variables inside the react component without
12// `useCallback` or `useMemo` because everytime these are called they
13// create a new selector with a blank cache.
14// It's safer to come up with a way to define these outside the
15// react component.
16const selectWhenTodos = createSelectorTodosByText();
17const selectWhereTodos = createSelectorTodosByText();
18
19const Page = () => {
20 const whenTodos = useSelector((state: State) =>
21 selectWhenTodos(state, { search: "when" })
22 );
23 const whereTodos = useSelector((state: State) =>
24 selectWhereTodos(state, { search: "where" })
25 );
26
27 // rendering both todos on the page
28};
This is great because now we have two separate memoized selectors that we can use in this react component without popping their cache.
Avoid calling createSelector
inside a react component #
Calling createSelector
within a react component creates a new memoized
selector on every single run of the component. This defeats the purpose of using
reselect.
Example #
1const makeSelectTodosById = (id: string) => {
2 return createSelector(selectTodos, (todos) => todos[id]);
3};
4
5const ToDoPage = (props: { id: string }) => {
6 // this **creates** a new memoized selector everytime the react component
7 // re-renders, which wipes the cache for the selector!
8 const todo = useSelector(makeSelectTodosById(props.id));
9};
Selector builders are not a good way to pass props into the selector.
Passing props to a selector #
If we want to pass props into a selector, build a selector like this:
1const selectPropId = (state: State, { id }: { id: string }) => id;
2const selectTodoById = createSelector(
3 selectTodos,
4 selectPropId,
5 (todos, id) => todos[id],
6);
7
8const ToDoPage = (props: { id: string }) => {
9 const todo = useSelector((state: State) =>
10 selectTodoById(state, { id: prop.id })
11 );
12};
When to use createSelector or useMemo #
With the rapid adoption of react hooks and t he introduction of useMemo
, one
might ask:
do we need createSelector anymore?
I think this topic warrants its own post but I will briefly discuss my thoughts
on the topic.
Both createSelector
and useMemo
cache the result of some computation. With
createSelector
the function is created once in memory and then used throughout
the entire application. As we have seen, when we need to memoize more than one
call to createSelector
then we need to create a selector factory. With
useMemo
, on the other hand, the memoization function is created within the
main component render function. react
has some magic to make this work
correctly that I won't go into, but feel free to read Dan's
Why do hooks rely on call order?
There's a cost to using useMemo
and
recent benchmarks suggest that useMemo
should be used sparringly.
Because of react
's magic in order to get hooks to work with their current API,
there's a cost to using them.
Basically, the decision tree for using createSelector
vs useMemo
should look
something like this:
The simplest heuristic I can come up with:
If you can find a way to use
createSelector
instead ofuseMemo
then that is preferred.
I plan on writing a follow-up article on this topic that goes deeper into the
performance differences between createSelector
and useMemo
.
How to use selectors inside redux-saga #
Using selectors inside a saga is pretty simple. redux-saga provides a helper function called select which will automatically pass the state to the variable.
1import { select } from "redux-saga/effects";
2
3const selectToken = (state: State) => state.token;
4const selectUserById = (state: State, props: { id: string }) => state.users[id];
5
6function* onLogin() {
7 const token = yield select(selectToken);
8 const userId = yield select(selectUserbyId, { id });
9}