Time & space efficient state selectors for React, Redux, and more
npm install selectre
import { createSelector } from "selectre";
// use API similar to Reselect
let selectCurrentUser = createSelector(
// to make simple selectors without hustle
(state) => state.users.currentUser,
);
let selectProjectById = createSelector(
// seamlessly use parameters as selector input
(state, projectId) => state.projects.byId[projectId],
// pass more inputs to grab data from different state branches
(state) => state.meta,
// don't worry about returning complex data
(projectInfo, meta) => ({ ...projectInfo, ...meta }),
);
function ProjectInfo({ projectId }) {
// call the select function to get a properly cached selector
let currentUser = useSelector(selectCurrentUser());
// which also allows you to pass parameters, if there any
let projectInfo = useSelector(selectProjectById(projectId));
return <JSX />;
}
useSelector()
and React's useSyncExternalStore()
and ensure the least amount of unnecessary computations for things that don't change. 🚧 work in progress 🚧
🚧 work in progress 🚧
Selectre includes necessary type definitions to enhance developers experience when using TypeScript. Writing selectors in TypeScript, there are a couple of things you may encounter and this guide should help addressing them.
State parameter should always be typed. Selectors do not know where the state is coming from and who is calling them. To improve developers experience it is better to explicitly define state's type:
let selectData = createSelector(
(state: MyStateType) => state.a.b.c.data,
);
Parametric selectors should enumerate all parameters in the first input. The way how type definitions are written, TypeScript expects all input functions to have the same signatures (because all of them receive the same parameters during computation). It is likely that some inputs may not use all parameters, so it is fine to skip some of them. But TypeScript relies on the very first input of a selector to determine what parameters to look for in the following inputs and in the selector signature:
let selectDataByParams = createSelector(
(state: MyState, id: string, offset: number) => state.data.slice(offset, 10),
(state: MyState, id: string) => state.some.other.stuff[id],
(data, stuff) => [data, stuff],
);
O(n)
for get and O(n)
for set operations. Frequent use of array's splice()
and unshift()
in Reselect is an unnecessary performance burden useSelector()
which means additional selector reads during forced re-render createSelector()
in Selectre is not selector itself but an accessor to the selector and its cached result Getting into more details, let's consider the example that was described before:
import { createSelector } from "selectre";
import { useSelector } from "react-redux";
let selectNumberFilteredTodos = createSelector(
(state) => state.todos,
(_, completed) => completed,
(todos, completed) => todos.filter((todo) => todo.completed === completed).length,
);
function TodoCounter({ completed }) {
let numberFilteredTodos = useSelector(selectNumberFilteredTodos(completed));
return <span>{numberFilteredTodos}</span>;
}
A simple case of a selector with parameters, being used with Redux's useSelector()
. Values in the selector are compared using shallow equality by default, nothing needs to be configured manually. If you want to have the same behavior implemented with Reselect, here is what needs to be done:
import { useMemo } from "react";
import { createSelector } from "reselect";
// 1. Need to explicitly set shallowEqual as a second param of useSelector
import { shallowEqual, useSelector } from "react-redux";
// 2. need to make a factory function, because selector instances can't be shared by default
let makeSelectNumberFilteredTodos = () =>
createSelector(
(state) => state.todos,
(_, completed) => completed,
(todos, completed) => todos.filter((todo) => todo.completed === completed).length,
);
function TodoCounter({ completed }) {
// 3. additional friction in order to start using a selector
let selectNumberFilteredTodos = useMemo(makeSelectNumberFilteredTodos, []);
// 4. still need to use arrow function in useSelector() which means additional read operation
let numberFilteredTodos = useSelector(
(state) => selectNumberFilteredTodos(state, completed),
shallowEqual,
);
return <span>{numberFilteredTodos}</span>;
}
This simple case requires developer to know plenty of details, just to make sure that a selector that returns an object does not affect performance. It is something that quite easy to overlook, e.g. when you only update selector's output from primitive value to object.
The intent and main API is similar and based on Reselect.
LRU cache implementation is based on Guillaume Plique's article about LRU cache and using typed arrays to implement Doubly-Linked Lists (GitHub: @Yomguithereal, Twitter: @Yomguithereal).
Cache key's hash function implementation is based on Immutable.js hashCode()
.