What is a selector?

· erock's devlog

A quick introduction to using selectors in a redux application
#redux #react

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:

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:

picture describing how to decide whether to use createSelector or useMemo

The simplest heuristic I can come up with:

If you can find a way to use createSelector instead of useMemo 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}

I have no idea what I'm doing. Subscribe to my rss feed to read more of my articles.