Security News
ESLint is Now Language-Agnostic: Linting JSON, Markdown, and Beyond
ESLint has added JSON and Markdown linting support with new officially-supported plugins, expanding its versatility beyond JavaScript.
Reselect is a library for building memoized selectors in Redux. Selectors can compute derived data, allowing Redux to store the minimal possible state. Reselect selectors can be used to efficiently compute derived data from the Redux store.
Memoization
Reselect provides memoization for selectors. Selectors created with `createSelector` only recompute when their inputs change, which saves on unnecessary recalculations.
const getVisibleTodos = createSelector(
[getVisibilityFilter, getTodos],
(visibilityFilter, todos) => {
switch (visibilityFilter) {
case 'SHOW_ALL':
return todos;
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed);
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed);
}
}
)
Composability
Selectors can be composed together. One selector can use the output of another selector as input, allowing for complex queries to be built up in a clean and maintainable way.
const getTodoList = state => state.todoList;
const getKeyword = state => state.keyword;
const getFilteredTodoList = createSelector(
[getTodoList, getKeyword],
(todoList, keyword) => todoList.filter(todo => todo.includes(keyword))
);
Performance Optimization
Reselect helps in optimizing performance by avoiding unnecessary computations. This is particularly useful when dealing with expensive operations that should not be run every time the state updates.
const expensiveSelector = createSelector(
state => state.items,
items => expensiveComputation(items)
);
Re-reselect is an extension of Reselect that allows for creating memoized selectors with a cache size greater than one. This is useful for memoizing selectors with different argument combinations.
Redux-ORM is a library that helps to manage relational data in Redux. It provides a more ORM-like experience for managing entities and their relationships. While it doesn't provide the same selector functionality as Reselect, it can be used alongside Reselect to manage complex data structures.
Memoize-one is a simple memoization library that remembers the latest arguments and result. It's not specifically designed for Redux or selectors, but it can be used to memoize any function, similar to how Reselect memoizes selectors.
A library for creating memoized "selector" functions. Commonly used with Redux, but usable with any plain JS immutable data as well.
The Redux docs usage page on Deriving Data with Selectors covers the purpose and motivation for selectors, why memoized selectors are useful, typical Reselect usage patterns, and using selectors with React-Redux.
While Reselect is not exclusive to Redux, it is already included by default in the official Redux Toolkit package - no further installation needed.
import { createSelector } from '@reduxjs/toolkit'
For standalone usage, install the reselect
package:
# NPM
npm install reselect
# Yarn
yarn add reselect
# Bun
bun add reselect
# PNPM
pnpm add reselect
Reselect exports a createSelector
API, which generates memoized selector functions. createSelector
accepts one or more input selectors, which extract values from arguments, and a result function function that receives the extracted values and should return a derived value. If the generated output selector is called multiple times, the output will only be recalculated when the extracted values have changed.
You can play around with the following example in this CodeSandbox:
import { createSelector } from 'reselect'
interface RootState {
todos: { id: number; completed: boolean }[]
alerts: { id: number; read: boolean }[]
}
const state: RootState = {
todos: [
{ id: 0, completed: false },
{ id: 1, completed: true }
],
alerts: [
{ id: 0, read: false },
{ id: 1, read: true }
]
}
const selectCompletedTodos = (state: RootState) => {
console.log('selector ran')
return state.todos.filter(todo => todo.completed === true)
}
selectCompletedTodos(state) // selector ran
selectCompletedTodos(state) // selector ran
selectCompletedTodos(state) // selector ran
const memoizedSelectCompletedTodos = createSelector(
[(state: RootState) => state.todos],
todos => {
console.log('memoized selector ran')
return todos.filter(todo => todo.completed === true)
}
)
memoizedSelectCompletedTodos(state) // memoized selector ran
memoizedSelectCompletedTodos(state)
memoizedSelectCompletedTodos(state)
console.log(selectCompletedTodos(state) === selectCompletedTodos(state)) //=> false
console.log(
memoizedSelectCompletedTodos(state) === memoizedSelectCompletedTodos(state)
) //=> true
As you can see from the example above, memoizedSelectCompletedTodos
does not run the second or third time, but we still get the same return value as last time.
In addition to skipping unnecessary recalculations, memoizedSelectCompletedTodos
returns the existing result reference if there is no recalculation. This is important for libraries like React-Redux or React that often rely on reference equality checks to optimize UI updates.
Type instantiation is excessively deep and possibly infinite
createSelector
for my root state?createSelector
without memoization?createSelector
, and are called with all selector arguments. They are responsible for extracting and providing necessary values to the result function.createSelector
.Dependencies
: Same as input selectors. They are what the output selector "depends" on.The below example serves as a visual aid:
const outputSelector = createSelector(
[inputSelector1, inputSelector2, inputSelector3], // synonymous with `dependencies`.
resultFunc // Result function
)
Reselect, at its core, is a library for creating memoized selectors in JavaScript applications. Its primary role is to efficiently compute derived data based on provided inputs. A key aspect of Reselect's internal mechanism is how it orchestrates the flow of arguments from the final selector to its constituent input selectors.
const finalSelector = (...args) => {
const extractedValues = inputSelectors.map(inputSelector =>
inputSelector(...args)
)
return resultFunc(...extractedValues)
}
In this pattern, the finalSelector
is composed of several input selectors, all receiving the same arguments as the final selector. Each input selector processes its part of the data, and the results are then combined and further processed by the result function. Understanding this argument flow is crucial for appreciating how Reselect optimizes data computation and minimizes unnecessary recalculations.
Reselect uses a two-stage "cascading" approach to memoizing functions:
The way Reselect works can be broken down into multiple parts:
Initial Run: On the first call, Reselect runs all the input selectors, gathers their results, and passes them to the result function.
Subsequent Runs: For subsequent calls, Reselect performs two levels of checks:
First Level: It compares the current arguments with the previous ones (done by argsMemoize
).
If they're the same, it returns the cached result without running the input selectors or the result function.
If they differ, it proceeds ("cascades") to the second level.
Second Level: It runs the input selectors and compares their current results with the previous ones (done by memoize
).
[!NOTE] If any one of the input selectors return a different result, all input selectors will recalculate.
This behavior is what we call Cascading Double-Layer Memoization.
Standard memoization only compares arguments. If they're the same, it returns the cached result.
Reselect adds a second layer of checks with the input selectors. This is crucial in Redux applications where state references change frequently.
A normal memoization function will compare the arguments, and if they are the same as last time, it will skip running the function and return the cached result. However, Reselect enhances this by introducing a second tier of checks via its input selectors. It's possible that the arguments passed to these input selectors may change, yet their results remain the same. When this occurs, Reselect avoids re-executing the result function, and returns the cached result.
This feature becomes crucial in Redux applications, where the state
changes its reference anytime an action
is dispatched.
[!NOTE] The input selectors take the same arguments as the output selector.
While Reselect can be used independently from Redux, it is a standard tool used in most Redux applications to help optimize calculations and UI updates:
Imagine you have a selector like this:
const selectCompletedTodos = (state: RootState) =>
state.todos.filter(todo => todo.completed === true)
So you decide to memoize it:
const selectCompletedTodos = someMemoizeFunction((state: RootState) =>
state.todos.filter(todo => todo.completed === true)
)
Then you update state.alerts
:
store.dispatch(toggleRead(0))
Now when you call selectCompletedTodos
, it re-runs, because we have effectively broken memoization.
selectCompletedTodos(store.getState())
// Will not run, and the cached result will be returned.
selectCompletedTodos(store.getState())
store.dispatch(toggleRead(0))
// It recalculates.
selectCompletedTodos(store.getState())
But why? selectCompletedTodos
only needs to access state.todos
, and has nothing to do with state.alerts
, so why have we broken memoization? Well that's because in Redux anytime you make a change to the root state
, it gets shallowly updated, which means its reference changes, therefore a normal memoization function will always fail the comparison check on the arguments.
But with Reselect, we can do something like this:
const selectCompletedTodos = createSelector(
[(state: RootState) => state.todos],
todos => todos.filter(todo => todo.completed === true)
)
And now we have achieved memoization:
selectCompletedTodos(store.getState())
// Will not run, and the cached result will be returned.
selectCompletedTodos(store.getState())
store.dispatch(toggleRead(0))
// The `input selectors` will run, but the `result function` is
// skipped and the cached result will be returned.
selectCompletedTodos(store.getState())
Even when the overall state
changes, Reselect ensures efficient memoization through its unique approach. The result function doesn't re-run if the relevant part of the state
(in this case state.todos
), remains unchanged. This is due to Reselect's Cascading Double-Layer Memoization. The first layer checks the entire state
, and the second layer checks the results of the input selectors. If the first layer fails (due to a change in the overall state
) but the second layer succeeds (because state.todos
is unchanged), Reselect skips recalculating the result function. This dual-check mechanism makes Reselect particularly effective in Redux applications, ensuring computations are only done when truly necessary.
Description
Accepts one or more "input selectors" (either as separate arguments or a single array), a single "result function", and an optional options object, and generates a memoized selector function.
Parameters
Name | Description |
---|---|
inputSelectors | An array of input selectors, can also be passed as separate arguments. |
resultFunc | A function that takes the results of the input selectors as separate arguments. |
createSelectorOptions? | An optional options object that allows for further customization per selector. |
Returns
A memoized output selector.
Name | Description |
---|---|
InputSelectors | The type of the input selectors array. |
Result | The return type of the result function as well as the output selector. |
OverrideMemoizeFunction | The type of the optional memoize function that could be passed into the options object to override the original memoize function that was initially passed into createSelectorCreator . |
OverrideArgsMemoizeFunction | The type of the optional argsMemoize function that could be passed into the options object to override the original argsMemoize function that was initially passed into createSelectorCreator . |
Description
Accepts either a memoize
function and ...memoizeOptions
rest parameter, or since 5.0.0 an options
object containing a memoize
function and creates a custom selector creator function.
Parameters (since 5.0.0)
Name | Description |
---|---|
options | An options object containing the memoize function responsible for memoizing the resultFunc inside createSelector (e.g., defaultMemoize or weakMapMemoize ). It also provides additional options for customizing memoization. While the memoize property is mandatory, the rest are optional. |
options.argsMemoize? | The optional memoize function that is used to memoize the arguments passed into the output selector generated by createSelector (e.g., defaultMemoize or weakMapMemoize ). Default defaultMemoize |
options.argsMemoizeOptions? | Optional configuration options for the argsMemoize function. These options are passed to the argsMemoize function as the second argument. since 5.0.0 |
options.devModeChecks? | Overrides the settings for the global development mode checks for the selector. since 5.0.0 |
options.memoize | The memoize function that is used to memoize the resultFunc inside createSelector (e.g., defaultMemoize or weakMapMemoize ). since 5.0.0 |
options.memoizeOptions? | Optional configuration options for the memoize function. These options are passed to the memoize function as the second argument. since 5.0.0 |
Parameters
Name | Description |
---|---|
memoize | The memoize function responsible for memoizing the resultFunc inside createSelector (e.g., defaultMemoize or weakMapMemoize ). |
...memoizeOptionsFromArgs | Optional configuration options for the memoization function. These options are then passed to the memoize function as the second argument onwards. |
Returns
A customized createSelector
function.
Name | Description |
---|---|
MemoizeFunction | The type of the memoize function that is used to memoize the resultFunc inside createSelector (e.g., defaultMemoize or weakMapMemoize ). |
ArgsMemoizeFunction | The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by createSelector (e.g., defaultMemoize or weakMapMemoize ). If none is explicitly provided, weakMapMemoize will be used. |
options
(since 5.0.0)const customCreateSelector = createSelectorCreator({
memoize: customMemoize, // Function to be used to memoize `resultFunc`
memoizeOptions: [memoizeOption1, memoizeOption2], // Options passed to `customMemoize` as the second argument onwards
argsMemoize: customArgsMemoize, // Function to be used to memoize the selector's arguments
argsMemoizeOptions: [argsMemoizeOption1, argsMemoizeOption2] // Options passed to `customArgsMemoize` as the second argument onwards
})
const customSelector = customCreateSelector(
[inputSelector1, inputSelector2],
resultFunc // `resultFunc` will be passed as the first argument to `customMemoize`
)
customSelector(
...selectorArgs // Will be memoized by `customArgsMemoize`
)
memoize
and ...memoizeOptions
createSelectorCreator
can be used to make a customized version of createSelector
.
The memoize
argument is a memoization function to replace weakMapMemoize
.
The ...memoizeOptions
rest parameters are zero or more configuration options to be passed to memoizeFunc
. The selectors resultFunc
is passed as the first argument to memoize
and the memoizeOptions
are passed as the second argument onwards:
const customSelectorCreator = createSelectorCreator(
customMemoize, // Function to be used to memoize `resultFunc`
option1, // `option1` will be passed as second argument to `customMemoize`
option2, // `option2` will be passed as third argument to `customMemoize`
option3 // `option3` will be passed as fourth argument to `customMemoize`
)
const customSelector = customSelectorCreator(
[inputSelector1, inputSelector2],
resultFunc // `resultFunc` will be passed as first argument to `customMemoize`
)
Internally customSelector
calls the memoize function as follows:
customMemoize(resultFunc, option1, option2, option3)
equalityCheck
for defaultMemoize
import { createSelectorCreator, defaultMemoize } from 'reselect'
import isEqual from 'lodash.isequal'
// create a "selector creator" that uses lodash.isequal instead of ===
const createDeepEqualSelector = createSelectorCreator(defaultMemoize, isEqual)
// use the new "selector creator" to create a selector
const selectSum = createDeepEqualSelector(
[state => state.values.filter(val => val < 5)],
values => values.reduce((acc, val) => acc + val, 0)
)
import { createSelectorCreator } from 'reselect'
import memoize from 'lodash.memoize'
const hashFn = (...args) =>
args.reduce((acc, val) => acc + '-' + JSON.stringify(val), '')
const customSelectorCreator = createSelectorCreator(memoize, hashFn)
const selector = customSelectorCreator(
[state => state.a, state => state.b],
(a, b) => a + b
)
Description
A convenience function that simplifies returning an object made up of selector results.
Parameters
Name | Description |
---|---|
inputSelectorsObject | A key value pair consisting of input selectors. |
selectorCreator? | A custom selector creator function. It defaults to createSelector . |
Returns
A memoized structured selector.
Name | Description |
---|---|
InputSelectorsObject | The shape of the input selectors object. |
MemoizeFunction | The type of the memoize function that is used to create the structured selector. It defaults to weakMapMemoize . |
ArgsMemoizeFunction | The type of the of the memoize function that is used to memoize the arguments passed into the generated structured selector. It defaults to weakMapMemoize . |
import { createSelector, createStructuredSelector } from 'reselect'
interface RootState {
todos: {
id: number
completed: boolean
title: string
description: string
}[]
alerts: { id: number; read: boolean }[]
}
// This:
const structuredSelector = createStructuredSelector(
{
todos: (state: RootState) => state.todos,
alerts: (state: RootState) => state.alerts,
todoById: (state: RootState, id: number) => state.todos[id]
},
createSelector
)
// Is essentially the same as this:
const selector = createSelector(
[
(state: RootState) => state.todos,
(state: RootState) => state.alerts,
(state: RootState, id: number) => state.todos[id]
],
(todos, alerts, todoById) => {
return {
todos,
alerts,
todoById
}
}
)
In your component:
interface Props {
id: number
}
const MyComponent: FC<Props> = ({ id }) => {
const { todos, alerts, todoById } = useSelector(state =>
structuredSelector(state, id)
)
return (
<div>
Next to do is:
<h2>{todoById.title}</h2>
<p>Description: {todoById.description}</p>
<ul>
<h3>All other to dos:</h3>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</div>
)
}
const selectA = state => state.a
const selectB = state => state.b
// The result function in the following selector
// is simply building an object from the input selectors
const structuredSelector = createSelector(selectA, selectB, (a, b) => ({
a,
b
}))
const result = structuredSelector({ a: 1, b: 2 }) // will produce { x: 1, y: 2 }
Reselect comes with a selection of memoization functions, each uniquely designed to address different scenarios and performance requirements. By effectively leveraging these functions, you can significantly enhance the efficiency and responsiveness of your applications.
Description
The standard memoize function used by createSelector
.
It has a default cache size of 1. This means it always recalculates when the value of an argument changes. However, this can be customized as needed with a specific max cache size (since 4.1.0).
It determines if an argument has changed by calling the equalityCheck
function. As defaultMemoize
is designed to be used with immutable data, the default equalityCheck
function checks for changes using reference equality:
const defaultEqualityCheck = (previousValue: any, currentValue: any) => {
return previousValue === currentValue
}
Parameters
Name | Description |
---|---|
func | The function to be memoized. |
equalityCheckOrOptions | Either an equality check function or an options object. |
Since 4.1.0, defaultMemoize
also accepts an options object as its first argument instead of an equalityCheck
function. The options
object may contain:
type EqualityFn = (a: any, b: any) => boolean
interface DefaultMemoizeOptions {
equalityCheck?: EqualityFn
resultEqualityCheck?: EqualityFn
maxSize?: number
}
Name | Description |
---|---|
equalityCheck | Used to compare the individual arguments of the provided calculation function. Default = defaultEqualityCheck |
resultEqualityCheck | If provided, used to compare a newly generated output value against previous values in the cache. If a match is found, the old value is returned. This addresses the common todos.map(todo => todo.id) use case, where an update to another field in the original data causes a recalculation due to changed references, but the output is still effectively the same. |
maxSize | The cache size for the selector. If greater than 1, the selector will use an LRU cache internally. Default = 1 |
[!WARNING] If
resultEqualityCheck
is used insideargsMemoizeOptions
it has no effect.
Returns
A memoized function with a .clearCache()
method attached.
Name | Description |
---|---|
Func | The type of the function that is memoized. |
defaultMemoize
with createSelector
import { shallowEqual } from 'react-redux'
import { createSelector } from 'reselect'
const selectTodoIds = createSelector(
[(state: RootState) => state.todos],
todos => todos.map(todo => todo.id),
{
memoizeOptions: {
equalityCheck: shallowEqual,
resultEqualityCheck: shallowEqual,
maxSize: 10
},
argsMemoizeOptions: {
equalityCheck: shallowEqual,
resultEqualityCheck: shallowEqual,
maxSize: 10
}
}
)
defaultMemoize
with createSelectorCreator
import { shallowEqual } from 'react-redux'
import { createSelectorCreator, defaultMemoize } from 'reselect'
const createSelectorShallowEqual = createSelectorCreator({
memoize: defaultMemoize,
memoizeOptions: {
equalityCheck: shallowEqual,
resultEqualityCheck: shallowEqual,
maxSize: 10
},
argsMemoize: defaultMemoize,
argsMemoizeOptions: {
equalityCheck: shallowEqual,
resultEqualityCheck: shallowEqual,
maxSize: 10
}
})
const selectTodoIds = createSelectorShallowEqual(
[(state: RootState) => state.todos],
todos => todos.map(todo => todo.id)
)
Description
defaultMemoize
has to be explicitly configured to have a cache size larger than 1, and uses an LRU cache internally.
weakMapMemoize
creates a tree of WeakMap
-based cache nodes based on the identity of the arguments it's been called with (in this case, the extracted values from your input selectors). This allows weakMapMemoize
to have an effectively infinite cache size. Cache results will be kept in memory as long as references to the arguments still exist, and then cleared out as the arguments are garbage-collected.
Pros:
WeakMap
s.Cons:
useSelector(state => selectSomeData(state, id))
Prior to weakMapMemoize
, you had this problem:
interface RootState {
items: { id: number; category: string; name: string }[]
}
const selectItemsByCategory = createSelector(
[
(state: RootState) => state.items,
(state: RootState, category: string) => category
],
(items, category) => items.filter(item => item.category === category)
)
selectItemsByCategory(state, 'Electronics') // Selector runs
selectItemsByCategory(state, 'Electronics')
selectItemsByCategory(state, 'Stationery') // Selector runs
selectItemsByCategory(state, 'Electronics') // Selector runs again!
Before you could solve this in a number of different ways:
maxSize
with defaultMemoize
:const selectItemsByCategory = createSelector(
[
(state: RootState) => state.items,
(state: RootState, category: string) => category
],
(items, category) => items.filter(item => item.category === category),
{
memoizeOptions: {
maxSize: 10
}
}
)
But this required having to know the cache size ahead of time.
useMemo
.const makeSelectItemsByCategory = (category: string) =>
createSelector([(state: RootState) => state.items], items =>
items.filter(item => item.category === category)
)
interface Props {
category: string
}
const MyComponent: FC<Props> = ({ category }) => {
const selectItemsByCategory = useMemo(
() => makeSelectItemsByCategory(category),
[category]
)
const itemsByCategory = useSelector(selectItemsByCategory)
return (
<div>
{itemsByCategory.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
)
}
useCallback
.const selectItemsByCategory = createSelector(
[
(state: RootState) => state.items,
(state: RootState, category: string) => category
],
(items, category) => items.filter(item => item.category === category)
)
const MyComponent: FC<Props> = ({ category }) => {
const selectItemsByCategoryMemoized = useCallback(selectItemsByCategory, [])
const itemsByCategory = useSelector(state =>
selectItemsByCategoryMemoized(state, category)
)
return (
<div>
{itemsByCategory.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
)
}
re-reselect
:import { createCachedSelector } from 're-reselect'
const selectItemsByCategory = createCachedSelector(
[
(state: RootState) => state.items,
(state: RootState, category: string) => category
],
(items, category) => items.filter(item => item.category === category)
)((state: RootState, category: string) => category)
Starting in 5.0.0, you can eliminate this problem using weakMapMemoize
.
const selectItemsByCategory = createSelector(
[
(state: RootState) => state.items,
(state: RootState, category: string) => category
],
(items, category) => items.filter(item => item.category === category),
{
memoize: weakMapMemoize,
argsMemoize: weakMapMemoize
}
)
selectItemsByCategory(state, 'Electronics') // Selector runs
selectItemsByCategory(state, 'Electronics') // Cached
selectItemsByCategory(state, 'Stationery') // Selector runs
selectItemsByCategory(state, 'Electronics') // Still cached!
This solves the problem of having to know and set the cache size prior to creating a memoized selector. Because weakMapMemoize
essentially provides a dynamic cache size out of the box.
Parameters
Name | Description |
---|---|
func | The function to be memoized. |
Returns
A memoized function with a .clearCache()
method attached.
Name | Description |
---|---|
Func | The type of the function that is memoized. |
weakMapMemoize
with createSelector
import { createSelector, weakMapMemoize } from 'reselect'
const selectItemsByCategory = createSelector(
[
(state: RootState) => state.items,
(state: RootState, category: string) => category
],
(items, category) => items.filter(item => item.category === category),
{
memoize: weakMapMemoize,
argsMemoize: weakMapMemoize
}
)
selectItemsByCategory(state, 'Electronics') // Selector runs
selectItemsByCategory(state, 'Electronics')
selectItemsByCategory(state, 'Stationery') // Selector runs
selectItemsByCategory(state, 'Electronics')
weakMapMemoize
with createSelectorCreator
import { createSelectorCreator, weakMapMemoize } from 'reselect'
const createSelectorWeakMap = createSelectorCreator({
memoize: weakMapMemoize,
argsMemoize: weakMapMemoize
})
const selectItemsByCategory = createSelectorWeakMap(
[
(state: RootState) => state.items,
(state: RootState, category: string) => category
],
(items, category) => items.filter(item => item.category === category)
)
selectItemsByCategory(state, 'Electronics') // Selector runs
selectItemsByCategory(state, 'Electronics')
selectItemsByCategory(state, 'Stationery') // Selector runs
selectItemsByCategory(state, 'Electronics')
Description
Uses an "auto-tracking" approach inspired by the work of the Ember Glimmer team. It uses a Proxy to wrap arguments and track accesses to nested fields in your selector on first read. Later, when the selector is called with new arguments, it identifies which accessed fields have changed and only recalculates the result if one or more of those accessed fields have changed. This allows it to be more precise than the shallow equality checks in defaultMemoize
.
[!WARNING] This API is still experimental and undergoing testing.
Pros:
defaultMemoize
will, which may also result in fewer component re-renders.Cons:
defaultMemoize
, because it has to do more work. (How much slower is dependent on the number of accessed fields in a selector, number of calls, frequency of input changes, etc)createSelector([state => state.todos], todos => todos)
that just immediately returns the extracted value will never update, because it doesn't see any field accesses to check.
Parameters
Name | Description |
---|---|
func | The function to be memoized. |
Returns
A memoized function with a .clearCache()
method attached.
Name | Description |
---|---|
Func | The type of the function that is memoized. |
unstable_autotrackMemoize
with createSelector
import { unstable_autotrackMemoize, createSelector } from 'reselect'
const selectTodoIds = createSelector(
[(state: RootState) => state.todos],
todos => todos.map(todo => todo.id),
{ memoize: unstable_autotrackMemoize }
)
unstable_autotrackMemoize
with createSelectorCreator
import { unstable_autotrackMemoize, createSelectorCreator } from 'reselect'
const createSelectorAutotrack = createSelectorCreator({
memoize: unstable_autotrackMemoize
})
const selectTodoIds = createSelectorAutotrack(
[(state: RootState) => state.todos],
todos => todos.map(todo => todo.id)
)
Reselect includes extra checks in development mode to help catch and warn about mistakes in selector behavior.
inputStabilityCheck
Due to how Cascading Memoization works in Reselect, it is crucial that your input selectors do not return a new reference on each run. If an input selector always returns a new reference, like
state => ({ a: state.a, b: state.b })
or
state => state.todos.map(todo => todo.id)
that will cause the selector to never memoize properly.
Since this is a common mistake, we've added a development mode check to catch this. By default, createSelector
will now run the input selectors twice during the first call to the selector. If the result appears to be different for the same call, it will log a warning with the arguments and the two different sets of extracted input values.
type DevModeCheckFrequency = 'always' | 'once' | 'never'
Possible Values | Description |
---|---|
once | Run only the first time the selector is called. |
always | Run every time the selector is called. |
never | Never run the input stability check. |
[!IMPORTANT] The input stability check is automatically disabled in production environments.
You can configure this behavior in two ways:
setGlobalDevModeChecks
:A setGlobalDevModeChecks
function is exported from Reselect, which should be called with the desired setting.
import { setGlobalDevModeChecks } from 'reselect'
// Run only the first time the selector is called. (default)
setGlobalDevModeChecks({ inputStabilityCheck: 'once' })
// Run every time the selector is called.
setGlobalDevModeChecks({ inputStabilityCheck: 'always' })
// Never run the input stability check.
setGlobalDevModeChecks({ inputStabilityCheck: 'never' })
inputStabilityCheck
option directly to createSelector
:// Create a selector that double-checks the results of input selectors every time it runs.
const selectCompletedTodosLength = createSelector(
[
// ❌ Incorrect Use Case: This input selector will not be memoized properly since it always returns a new reference.
(state: RootState) =>
state.todos.filter(({ completed }) => completed === true)
],
completedTodos => completedTodos.length,
// Will override the global setting.
{ devModeChecks: { inputStabilityCheck: 'always' } }
)
[!WARNING] This will override the global input stability check set by calling
setGlobalDevModeChecks
.
identityFunctionCheck
When working with Reselect, it's crucial to adhere to a fundamental philosophy regarding the separation of concerns between extraction and transformation logic.
Extraction Logic: This refers to operations like state => state.todos
, which should be placed in input selectors. Extraction logic is responsible for retrieving or 'selecting' data from a broader state or dataset.
Transformation Logic: In contrast, transformation logic, such as todos => todos.map(({ id }) => id)
, belongs in the result function. This is where you manipulate, format, or transform the data extracted by the input selectors.
Most importantly, effective memoization in Reselect hinges on following these guidelines. Memoization, only functions correctly when extraction and transformation logic are properly segregated. By keeping extraction logic in input selectors and transformation logic in the result function, Reselect can efficiently determine when to reuse cached results and when to recompute them. This not only enhances performance but also ensures the consistency and predictability of your selectors.
For memoization to work as intended, it's imperative to follow both guidelines. If either is disregarded, memoization will not function properly. Consider the following example for clarity:
// ❌ Incorrect Use Case: This will not memoize correctly, and does nothing useful!
const brokenSelector = createSelector(
// ✔️ GOOD: Contains extraction logic.
[(state: RootState) => state.todos],
// ❌ BAD: Does not contain transformation logic.
todos => todos
)
type DevModeCheckFrequency = 'always' | 'once' | 'never'
Possible Values | Description |
---|---|
once | Run only the first time the selector is called. |
always | Run every time the selector is called. |
never | Never run the identity function check. |
[!IMPORTANT] The identity function check is automatically disabled in production environments.
You can configure this behavior in two ways:
setGlobalDevModeChecks
:import { setGlobalDevModeChecks } from 'reselect'
// Run only the first time the selector is called. (default)
setGlobalDevModeChecks({ identityFunctionCheck: 'once' })
// Run every time the selector is called.
setGlobalDevModeChecks({ identityFunctionCheck: 'always' })
// Never run the identity function check.
setGlobalDevModeChecks({ identityFunctionCheck: 'never' })
identityFunctionCheck
option directly to createSelector
:// Create a selector that checks to see if the result function is an identity function.
const selectTodos = createSelector(
[(state: RootState) => state.todos],
// This result function does not contain any transformation logic.
todos => todos,
// Will override the global setting.
{ devModeChecks: { identityFunctionCheck: 'always' } }
)
The output selectors created by createSelector
have several additional properties attached to them:
Name | Description |
---|---|
resultFunc | The final function passed to createSelector . |
memoizedResultFunc | The memoized version of resultFunc . |
lastResult | Returns the last result calculated by memoizedResultFunc . |
dependencies | The array of the input selectors used by createSelector to compose resultFunc . |
recomputations | Counts the number of times memoizedResultFunc has been recalculated. |
resetRecomputations | Resets the count of recomputations count to 0. |
dependencyRecomputations | Counts the number of times the input selectors (dependencies ) have been recalculated. This is distinct from recomputations , which tracks the recalculations of the result function. |
resetDependencyRecomputations | Resets the dependencyRecomputations count to 0. |
memoize | Function used to memoize the resultFunc . |
argsMemoize | Function used to memoize the arguments passed into the output selector. |
Version 5.0.0 introduces several new features and improvements:
Customization Enhancements:
createSelectorCreator
, allowing for customized memoize
and argsMemoize
functions, alongside their respective options (memoizeOptions
and argsMemoizeOptions
).createSelector
function now supports direct customization of memoize
and argsMemoize
within its options object.Memoization Functions:
weakMapMemoize
and unstable_autotrackMemoize
.memoize
and argsMemoize
into the output selector fields for debugging purposes.TypeScript Support and Performance:
Type instantiation is excessively deep and possibly infinite
error.Selector API Enhancements:
createStructuredSelector
due to its susceptibility to runtime errors.TypedStructuredSelectorCreator
utility type (currently a work-in-progress) to facilitate the creation of a pre-typed version of createStructuredSelector
for your root state.Additional Functionalities:
dependencyRecomputations
and resetDependencyRecomputations
to the output selector fields. These additions provide greater control and insight over input selectors, complementing the new argsMemoize
API.inputStabilityCheck
, a development tool that runs the input selectors twice using the same arguments and triggers a warning If they return differing results for the same call.identityFunctionCheck
, a development tool that checks to see if the result function returns its own input.These updates aim to enhance flexibility, performance, and developer experience. For detailed usage and examples, refer to the updated documentation sections for each feature.
Breaking Changes:
ParametricSelector
and OutputParametricSelector
types. Their functionalities are now integrated into Selector
and OutputSelector
respectively, which inherently support additional parameters.A somewhat common mistake is to write an input selector that extracts a value or does some derivation, and a result function that just returns its result:
// ❌ BROKEN: this will not memoize correctly, and does nothing useful!
const brokenSelector = createSelector(
[(state: RootState) => state.todos],
todos => todos
)
Any result function that just returns its inputs is incorrect! The result function should always have the transformation logic.
Similarly:
// ❌ BROKEN: this will not memoize correctly!
const brokenSelector = createSelector(
[(state: RootState) => state],
state => state.todos
)
To reduce recalculations, use a predefined empty array when array.filter
or similar methods result in an empty array.
So you can have a pattern like this:
interface RootState {
todos: {
id: number
title: string
description: string
completed: boolean
}[]
}
const EMPTY_ARRAY: [] = []
const selectCompletedTodos = createSelector(
[(state: RootState) => state.todos],
todos => {
const completedTodos = todos.filter(todo => todo.completed === true)
return completedTodos.length === 0 ? EMPTY_ARRAY : completedTodos
}
)
Or to avoid repetition, you can create a wrapper function and reuse it:
const EMPTY_ARRAY: [] = []
export const fallbackToEmptyArray = <T>(array: T[]) => {
return array.length === 0 ? EMPTY_ARRAY : array
}
const selectCompletedTodos = createSelector(
[(state: RootState) => state.todos],
todos => {
return fallbackToEmptyArray(todos.filter(todo => todo.completed === true))
}
)
This way if the result function returns an empty array twice in a row, your component will not re-render due to a stable empty array reference:
const completedTodos = selectCompletedTodos(store.getState())
store.dispatch(addTodo())
console.log(completedTodos === selectCompletedTodos(store.getState())) //=> true
There are a few details that will help you skip running as many functions as possible and get the best possible performance out of Reselect:
state => state.todos
or argument providers like (state, id) => id
. You should not be doing any sort of calculation inside input selectors, and you should definitely not be returning an object or array with a new reference each time.This:
// ✔️ This is optimal because we have less calculations in input selectors and more in the result function.
const selectorGood = createSelector(
[(state: RootState) => state.todos],
todos => someExpensiveComputation(todos)
)
Is preferable to this:
// ❌ This is not optimal!
const selectorBad = createSelector(
[(state: RootState) => someExpensiveComputation(state.todos)],
someOtherCalculation
)
Check that your memoization function is compatible with your state update function (i.e. the reducer if you are using Redux). For example, a selector created with createSelector
will not work with a state update function that mutates an existing object instead of creating a new one each time. createSelector
uses an identity check (===
) to detect that an input has changed, so mutating an existing object will not trigger the selector to recompute because mutating an object does not change its identity. Note that if you are using Redux, mutating the state object is almost certainly a mistake.
To address unexpected recomputations in your selector, first ensure that inputStabilityCheck
is set to either 'always'
or 'once'
. This setting aids in debugging by monitoring the stability of your inputs. Additionally, utilize output selector fields such as recomputations
, resetRecomputations
, dependencyRecomputations
, and resetDependencyRecomputations
. These tools help identify the source of the issue.
Keep an eye on the dependencyRecomputations
count. If it increases while recomputations
remains the same, it suggests that your arguments are changing references but your input selectors are stable which is typically the desired behavior.
To delve deeper, you can determine which arguments are changing references too frequently by using the argsMemoizeOptions
and equalityCheck
. Consider the following example:
interface RootState {
todos: { id: number; completed: boolean }[]
alerts: { id: number; read: boolean; type: string }[]
}
const selectAlertsByType = createSelector(
[
(state: RootState) => state.alerts,
(state: RootState, type: string) => type
],
(alerts, type) => alerts.filter(todo => todo.type === type),
{
argsMemoizeOptions: {
// This will check the arguments passed to the output selector.
equalityCheck: (a, b) => {
if (a !== b) {
console.log('Changed argument:', a, 'to', b)
}
return a === b
}
}
}
)
Yes. Reselect has no dependencies on any other package, so although it was designed to be used with Redux it can be used independently. It can be used with any plain JS data, such as typical React state values, as long as that data is being updated immutably.
Each of the input selectors you provide will be called with all of the selector's arguments. You can add additional input selectors to extract arguments and forward them to the result function, like this:
const selectTodosByCategory = createSelector(
(state: RootState) => state.todos,
// Extract the second argument to pass it on
(state: RootState, category: string) => category,
(todos, category) => todos.filter(t => t.category === category)
)
When creating a selector that accepts arguments in Reselect, it's important to structure your input and output selectors appropriately. Here are key points to consider:
Consistency in Arguments: Ensure that all positional arguments across input selectors are of the same type for consistency.
Selective Argument Usage: Design each selector to use only its relevant argument(s) and ignore the rest. This is crucial because all input selectors receive the same arguments that are passed to the output selector.
Suppose we have the following state structure:
interface RootState {
items: {
id: number
category: string
vendor: { id: number; name: string }
}[]
// ... other state properties ...
}
To create a selector that filters items
based on a category
and excludes a specific id
, you can set up your selectors as follows:
const selectAvailableItems = createSelector(
[
// First input selector extracts items from the state
(state: RootState) => state.items,
// Second input selector forwards the category argument
(state: RootState, category: string) => category,
// Third input selector forwards the ID argument
(state: RootState, category: string, id: number) => id
],
// Output selector uses the extracted items, category, and ID
(items, category, id) =>
items.filter(item => item.category === category && item.id !== id)
)
Internally Reselect is doing this:
// Input selector #1
const items = (state: RootState, category: string, id: number) => state.items
// Input selector #2
const category = (state: RootState, category: string, id: number) => category
// Input selector #3
const id = (state: RootState, category: string, id: number) => id
// result of output selector
const finalResult =
// The result function
items.filter(item => item.category === category && item.id !== id)
In this example, selectItemId
expects that its second argument will be some simple value, while selectVendorName
expects that the second argument is an object. If you call selectItemById(state, 42)
, selectVendorName
will break because it's trying to access 42.name
. Reselect's TS types should detect this and prevent compilation:
const selectItems = (state: RootState) => state.items
// expects a number as the second argument
const selectItemId = (state: RootState, itemId: number) => itemId
// expects an object as the second argument
const selectVendorName = (
state: RootState,
vendor: { id: number; name: string }
) => vendor.name
const selectItemById = createSelector(
[selectItems, selectItemId, selectVendorName],
(items, itemId, vendorName) => items[itemId]
)
Yes. The built-in defaultMemoize
memoizer works great for a lot of use cases, but it can be customized or swapped out for a different memoizer. See these examples.
Selectors are pure functions - for a given input, a selector should always produce the same result. For this reason they are simple to unit test: call the selector with a set of inputs, and assert that the result value matches an expected shape.
interface RootState {
todos: { id: number; completed: boolean }[]
alerts: { id: number; read: boolean }[]
}
const state: RootState = {
todos: [
{ id: 0, completed: false },
{ id: 1, completed: true }
],
alerts: [
{ id: 0, read: false },
{ id: 1, read: true }
]
}
// With `Vitest` or `Jest`
test('selector unit test', () => {
const selectTodoIds = createSelector(
[(state: RootState) => state.todos],
todos => todos.map(({ id }) => id)
)
const firstResult = selectTodoIds(state)
const secondResult = selectTodoIds(state)
// Reference equality should pass.
expect(firstResult).toBe(secondResult)
// Deep equality should also pass.
expect(firstResult).toStrictEqual(secondResult)
selectTodoIds(state)
selectTodoIds(state)
selectTodoIds(state)
// The `Result Function` should not recalculate.
expect(selectTodoIds.recomputations()).toBe(1)
// `input selectors` should not recalculate.
expect(selectTodoIds.dependencyRecomputations()).toBe(1)
})
// With `Chai`
test('selector unit test', () => {
const selectTodoIds = createSelector(
[(state: RootState) => state.todos],
todos => todos.map(({ id }) => id)
)
const firstResult = selectTodoIds(state)
const secondResult = selectTodoIds(state)
// Reference equality should pass.
expect(firstResult).to.equal(secondResult)
// Deep equality should also pass.
expect(firstResult).to.deep.equal(secondResult)
selectTodoIds(state)
selectTodoIds(state)
selectTodoIds(state)
// The `result function` should not recalculate.
expect(selectTodoIds.recomputations()).to.equal(1)
// `input selectors` should not recalculate.
expect(selectTodoIds.dependencyRecomputations()).to.equal(1)
})
Yes, although if they pass in different arguments, you will need to handle that in order for memoization to work consistently:
maxSize
if using defaultMemoize
( as of 4.1.0+)weakMapMemoize
(as of 5.0.0+)Yes! Reselect is now written in TypeScript itself, so they should Just Work™.
Type instantiation is excessively deep and possibly infinite
Starting in 5.0.0 you should be able to nest up to 30 selectors, but in case you still run into this issue, you can refer to this comment for a discussion of the problem, as relating to nested selectors.
Selectors that take arguments are commonly used inside of React-Redux's useSelector
by using a closure to pass along the extra arguments:
function TodosList({ category }) {
const filteredTodos = useSelector(state =>
selectTodosByCategory(state, category)
)
}
If you prefer to use a curried form instead, you can create a curried selector with this recipe:
You can try this pattern:
const currySelector = <
State,
Result,
Params extends readonly any[],
AdditionalFields
>(
selector: ((state: State, ...args: Params) => Result) & AdditionalFields
) => {
const curriedSelector = (...args: Params) => {
return (state: State) => {
return selector(state, ...args)
}
}
return Object.assign(curriedSelector, selector)
}
const selectTodoByIdCurried = currySelector(
createSelector(
[(state: RootState) => state.todos, (state: RootState, id: number) => id],
(todos, id) => todos.find(todo => todo.id === id)
)
)
Or for reusability you can do this:
import type { defaultMemoize, SelectorArray, UnknownMemoizer } from 'reselect'
import { createSelector } from 'reselect'
export const createCurriedSelector = <
InputSelectors extends SelectorArray,
Result,
OverrideMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize,
OverrideArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize
>(
...args: Parameters<
typeof createSelector<
InputSelectors,
Result,
OverrideMemoizeFunction,
OverrideArgsMemoizeFunction
>
>
) => {
return currySelector(createSelector(...args))
}
This:
const selectTodoById = createSelector(
[(state: RootState) => state.todos, (state: RootState, id: number) => id],
(todos, id) => todos.find(todo => todo.id === id)
)
selectTodoById(state, 0)
Is the same as this:
selectTodoByIdCurried(0)(state)
As before you had to do this:
const todoById = useSelector(state => selectTodoById(state, id))
Now you can do this:
const todoById = useSelector(selectTodoByIdCurried(id))
Another thing you can do if you are using React-Redux is create a custom hook factory function:
import { useSelector } from 'react-redux'
export const createParametricSelectorHook = <
Result,
Params extends readonly unknown[]
>(
selector: (state: RootState, ...params: Params) => Result
) => {
return (...args: Params) => {
return useSelector(state => selector(state, ...args))
}
}
const useSelectTodo = createParametricSelectorHook(selectTodoById)
And then inside your component:
import type { FC } from 'react'
interface Props {
id: number
}
const MyComponent: FC<Props> = ({ id }) => {
const todo = useSelectTodo(id)
return <div>{todo.title}</div>
}
createSelector
for my root state?When used with Redux, it's typical to have all input selectors take (state: RootState)
as their first argument. Creating a pre-typed version of createSelector
can shorten that repetition.
You can create a custom typed version of createSelector
by defining a utility type that extends the original createSelector
function. Here's an example:
import type {
OutputSelector,
Selector,
SelectorArray,
UnknownMemoizer
} from 'reselect'
import { createSelector } from 'reselect'
interface RootState {
todos: { id: number; completed: boolean }[]
alerts: { id: number; read: boolean }[]
}
export type TypedCreateSelector<
State,
MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize,
ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize
> = <
InputSelectors extends readonly Selector<State>[],
Result,
OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction,
OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction
>(
...createSelectorArgs: Parameters<
typeof createSelector<
InputSelectors,
Result,
OverrideMemoizeFunction,
OverrideArgsMemoizeFunction
>
>
) => ReturnType<
typeof createSelector<
InputSelectors,
Result,
OverrideMemoizeFunction,
OverrideArgsMemoizeFunction
>
>
export const createAppSelector: TypedCreateSelector<RootState> = createSelector
[!WARNING]: This approach currently only supports input selectors provided as a single array.
createSelector
without memoization?There may be rare cases when you might want to use createSelector
for its composition syntax, but without any memoization applied. In that case, create an identity function
and use it as the memoizers:
const identity = <Func extends (...args: any[]) => any>(func: Func) => func
const createNonMemoizedSelector = createSelectorCreator({
memoize: identity,
argsMemoize: identity
})
Enhances Reselect selectors by wrapping createSelector
and returning a memoized collection of selectors indexed with the cache key returned by a custom resolver function.
Useful to reduce selectors recalculation when the same selector is repeatedly called with one/few different arguments.
Flipper plugin and and the connect app for debugging selectors in React Native Apps.
Inspired by Reselect Tools, so it also has all functionality from this library and more, but only for React Native and Flipper.
MIT
Originally inspired by getters in NuclearJS, subscriptions in re-frame and this proposal from speedskater.
FAQs
Selectors for Redux.
The npm package reselect receives a total of 7,279,319 weekly downloads. As such, reselect popularity was classified as popular.
We found that reselect demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 6 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
ESLint has added JSON and Markdown linting support with new officially-supported plugins, expanding its versatility beyond JavaScript.
Security News
Members Hub is conducting large-scale campaigns to artificially boost Discord server metrics, undermining community trust and platform integrity.
Security News
NIST has failed to meet its self-imposed deadline of clearing the NVD's backlog by the end of the fiscal year. Meanwhile, CVE's awaiting analysis have increased by 33% since June.