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 install reselect
yarn 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 an
“output” selector that receives the extracted values and should return a
derived value. If the generated 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'
const selectShopItems = state => state.shop.items
const selectTaxPercent = state => state.shop.taxPercent
const selectSubtotal = createSelector(selectShopItems, items =>
.reduce((subtotal, item) => subtotal + item.value, 0)
items
)
const selectTax = createSelector(
,
selectSubtotal,
selectTaxPercent, taxPercent) => subtotal * (taxPercent / 100)
(subtotal
)
const selectTotal = createSelector(
,
selectSubtotal,
selectTax, tax) => ({ total: subtotal + tax })
(subtotal
)
const exampleState = {
shop: {
taxPercent: 8,
items: [
name: 'apple', value: 1.2 },
{ name: 'orange', value: 0.95 }
{
]
}
}
console.log(selectSubtotal(exampleState)) // 2.15
console.log(selectTax(exampleState)) // 0.172
console.log(selectTotal(exampleState)) // { total: 2.322 }
Accepts one or more “input selectors” (either as separate arguments or a single array), a single “output selector” / “result function”, and an optional options object, and generates a memoized selector function.
When the selector is called, each input selector will be called with all of the provided arguments. The extracted values are then passed as separate arguments to the output selector, which should calculate and return a final result. The inputs and result are cached for later use.
If the selector is called again with the same arguments, the previously cached result is returned instead of recalculating a new result.
createSelector
determines if the value returned by an
input-selector has changed between calls using reference equality
(===
). Inputs to selectors created with
createSelector
should be immutable.
By default, selectors created with createSelector
have a
cache size of 1. This means they always recalculate when the value of an
input-selector changes, as a selector only stores the preceding value of
each input-selector. This can be customized by passing a
selectorOptions
object with a memoizeOptions
field containing options for the built-in defaultMemoize
memoization function .
const selectValue = createSelector(
=> state.values.value1,
state => state.values.value2,
state , value2) => value1 + value2
(value1
)
// You can also pass an array of selectors
const selectTotal = createSelector(
=> state.values.value1, state => state.values.value2],
[state , value2) => value1 + value2
(value1
)
// Selector behavior can be customized
const customizedSelector = createSelector(
=> state.a,
state => state.b,
state , b) => a + b,
(a
{// New in 4.1: Pass options through to the built-in `defaultMemoize` function
memoizeOptions: {
equalityCheck: (a, b) => a === b,
maxSize: 10,
resultEqualityCheck: shallowEqual
}
} )
Selectors are typically called with a Redux state
value
as the first argument, and the input selectors extract pieces of the
state
object for use in calculations. However, it’s also
common to want to pass additional arguments, such as a value to filter
by. Since input selectors are given all arguments, they can extract the
additional arguments and pass them to the output selector:
const selectItemsByCategory = createSelector(
[// Usual first input - extract value from `state`
=> state.items,
state // Take the second arg, `category`, and forward to the output selector
, category) => category
(state,
]// Output selector gets (`items, category)` as args
, category) => items.filter(item => item.category === category)
(items )
defaultMemoize
memoizes the function passed in the func
parameter. It is the standard memoize function used by
createSelector
.
defaultMemoize
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 (new in
4.1).
defaultMemoize
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:
function defaultEqualityCheck(previousVal, currentVal) {
return currentVal === previousVal
}
As of Reselect 4.1, defaultMemoize
also accepts an
options object as its first argument instead of
equalityCheck
. The options object may contain:
interface DefaultMemoizeOptions {
?: EqualityFn
equalityCheck?: EqualityFn
resultEqualityCheck?: number
maxSize }
Available options are:
equalityCheck
: used to compare the individual arguments
of the provided calculation functionresultEqualityCheck
: 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 address the common
todos.map(todo => todo.id)
use case, where an update to
another field in the original data causes a recalculate due to changed
references, but the output is still effectively the same.maxSize
: the cache size for the selector. If
maxSize
is greater than 1, the selector will use an LRU
cache internallyThe returned memoized function will have a .clearCache()
method attached.
defaultMemoize
can also be used with
createSelectorCreator
to create a new selector factory that
always has the same settings for each selector.
createSelectorCreator
can be used to make a customized
version of createSelector
.
The memoize
argument is a memoization function to
replace defaultMemoize
.
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(
, // function to be used to memoize resultFunc
customMemoize, // option1 will be passed as second argument to customMemoize
option1, // option2 will be passed as third argument to customMemoize
option2// option3 will be passed as fourth argument to customMemoize
option3
)
const customSelector = customSelectorCreator(
,
input1,
input2// resultFunc will be passed as first argument to customMemoize
resultFunc )
Internally customSelector
calls the memoize function as
follows:
customMemoize(resultFunc, option1, option2, option3)
Here are some examples of how you might use
createSelectorCreator
:
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.values.filter(val => val < 5),
state => values.reduce((acc, val) => acc + val, 0)
values )
import { createSelectorCreator } from 'reselect'
import memoize from 'lodash.memoize'
let called = 0
const hashFn = (...args) =>
.reduce((acc, val) => acc + '-' + JSON.stringify(val), '')
argsconst customSelectorCreator = createSelectorCreator(memoize, hashFn)
const selector = customSelectorCreator(
=> state.a,
state => state.b,
state , b) => {
(a++
calledreturn a + b
} )
createStructuredSelector
is a convenience function for a
common pattern that arises when using Reselect. The selector passed to a
connect
decorator often just takes the values of its
input-selectors and maps them to keys in an object:
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 }))
createStructuredSelector
takes an object whose
properties are input-selectors and returns a structured selector. The
structured selector returns an object with the same keys as the
inputSelectors
argument, but with the selectors replaced
with their values.
const selectA = state => state.a
const selectB = state => state.b
const structuredSelector = createStructuredSelector({
x: selectA,
y: selectB
})
const result = structuredSelector({ a: 1, b: 2 }) // will produce { x: 1, y: 2 }
Structured selectors can be nested:
const nestedSelector = createStructuredSelector({
subA: createStructuredSelector({
,
selectorA
selectorB,
})subB: createStructuredSelector({
,
selectorC
selectorD
}) })
A: 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.
The following example defines a simple selector that determines if the first todo item in an array of todos has been completed:
const selectIsFirstTodoComplete = createSelector(
=> state.todos[0],
state => todo && todo.completed
todo )
The following state update function will not work
with selectIsFirstTodoComplete
:
export default function todos(state = initialState, action) {
switch (action.type) {
case COMPLETE_ALL:
const areAllMarked = state.every(todo => todo.completed)
// BAD: mutating an existing object
return state.map(todo => {
.completed = !areAllMarked
todoreturn todo
})
default:
return state
} }
The following state update function will work with
selectIsFirstTodoComplete
:
export default function todos(state = initialState, action) {
switch (action.type) {
case COMPLETE_ALL:
const areAllMarked = state.every(todo => todo.completed)
// GOOD: returning a new object each time with Object.assign
return state.map(todo =>
Object.assign({}, todo, {
completed: !areAllMarked
})
)
default:
return state
} }
If you are not using Redux and have a requirement to work with
mutable data, you can use createSelectorCreator
to replace
the default memoization function and/or use a different equality check
function. See here
and here for
examples.
A: 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
that recomputes
unexpectedly may be receiving a new object on each update whether the
values it contains have changed or not. createSelector
uses
an identity check (===
) to detect that an input has
changed, so returning a new object on each update means that the
selector will recompute on each update.
import { REMOVE_OLD } from '../constants/ActionTypes'
const initialState = [
{text: 'Use Redux',
completed: false,
id: 0,
timestamp: Date.now()
}
]
export default function todos(state = initialState, action) {
switch (action.type) {
case REMOVE_OLD:
return state.filter(todo => {
return todo.timestamp + 30 * 24 * 60 * 60 * 1000 > Date.now()
})default:
return state
} }
The following selector is going to recompute every time REMOVE_OLD is invoked because Array.filter always returns a new object. However, in the majority of cases the REMOVE_OLD action will not change the list of todos so the recomputation is unnecessary.
import { createSelector } from 'reselect'
const todosSelector = state => state.todos
export const selectVisibleTodos = createSelector(
,
todosSelector=> {
(todos) ...
} )
You can eliminate unnecessary recomputations by returning a new object from the state update function only when a deep equality check has found that the list of todos has actually changed:
import { REMOVE_OLD } from '../constants/ActionTypes'
import isEqual from 'lodash.isequal'
const initialState = [
{text: 'Use Redux',
completed: false,
id: 0,
timestamp: Date.now()
}
]
export default function todos(state = initialState, action) {
switch (action.type) {
case REMOVE_OLD:
const updatedState = state.filter(todo => {
return todo.timestamp + 30 * 24 * 60 * 60 * 1000 > Date.now()
})return isEqual(updatedState, state) ? state : updatedState
default:
return state
} }
Alternatively, the default equalityCheck
function in the
selector can be replaced by a deep equality check:
import { createSelectorCreator, defaultMemoize } from 'reselect'
import isEqual from 'lodash.isequal'
const selectTodos = state => state.todos
// 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 mySelector = createDeepEqualSelector(
,
todosSelector=> {
(todos) ...
} )
Always check that the cost of an alternative
equalityCheck
function or deep equality check in the state
update function is not greater than the cost of recomputing every time.
If recomputing every time does work out to be the cheaper option, it may
be that for this case Reselect is not giving you any benefit over
passing a plain mapStateToProps
function to
connect
.
A: 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.
As shown in the API reference section above, provide input selectors that extract the arguments and forward them to the output selector for calculation:
const selectItemsByCategory = createSelector(
[// Usual first input - extract value from `state`
=> state.items,
state // Take the second arg, `category`, and forward to the output selector
, category) => category
(state,
]// Output selector gets (`items, category)` as args
, category) => items.filter(item => item.category === category)
(items )
A: We think it works great for a lot of use cases, but sure. See these examples.
A: For a given input, a selector should always produce the same output. For this reason they are simple to unit test.
const selector = createSelector(
=> state.a,
state => state.b,
state , b) => ({
(ac: a * 2,
d: b * 3
})
)
test('selector unit test', () => {
.deepEqual(selector({ a: 1, b: 2 }), { c: 2, d: 6 })
assert.deepEqual(selector({ a: 2, b: 3 }), { c: 4, d: 9 })
assert })
It may also be useful to check that the memoization function for a
selector works correctly with the state update function (i.e. the
reducer if you are using Redux). Each selector has a
recomputations
method that will return the number of times
it has been recomputed:
suite('selector', () => {
let state = { a: 1, b: 2 }
const reducer = (state, action) => ({
a: action(state.a),
b: action(state.b)
})
const selector = createSelector(
=> state.a,
state => state.b,
state , b) => ({
(ac: a * 2,
d: b * 3
})
)
const plusOne = x => x + 1
const id = x => x
test('selector unit test', () => {
= reducer(state, plusOne)
state .deepEqual(selector(state), { c: 4, d: 9 })
assert= reducer(state, id)
state .deepEqual(selector(state), { c: 4, d: 9 })
assert.equal(selector.recomputations(), 1)
assert= reducer(state, plusOne)
state .deepEqual(selector(state), { c: 6, d: 12 })
assert.equal(selector.recomputations(), 2)
assert
}) })
Additionally, selectors keep a reference to the last result function
as .resultFunc
. If you have selectors composed of many
other selectors this can help you test each selector without coupling
all of your tests to the shape of your state.
For example if you have a set of selectors like this:
selectors.js
export const selectFirst = createSelector( ... )
export const selectSecond = createSelector( ... )
export const selectThird = createSelector( ... )
export const myComposedSelector = createSelector(
,
selectFirst,
selectSecond,
selectThird, second, third) => first * second < third
(first )
And then a set of unit tests like this:
test/selectors.js
// tests for the first three selectors...
test("selectFirst unit test", () => { ... })
test("selectSecond unit test", () => { ... })
test("selectThird unit test", () => { ... })
// We have already tested the previous
// three selector outputs so we can just call `.resultFunc`
// with the values we want to test directly:
test("myComposedSelector unit test", () => {
// here instead of calling selector()
// we just call selector.resultFunc()
assert(myComposedSelector.resultFunc(1, 2, 3), true)
assert(myComposedSelector.resultFunc(2, 2, 1), false)
})
Finally, each selector has a resetRecomputations
method
that sets recomputations back to 0. The intended use is for a complex
selector that may have many independent tests and you don’t want to
manually manage the computation count or create a “dummy” selector for
each test.
A: Yes, although it requires some planning.
As of Reselect 4.1, you can create a selector with a cache size
greater than one by passing in a maxSize
option under
memoizeOptions
for use with the built-in
defaultMemoize
.
Otherwise, selectors created using createSelector
only
have a cache size of one. This can make them unsuitable for sharing
across multiple instances if the arguments to the selector are different
for each instance of the component. There are a couple of ways to get
around this:
useMemo
hook to generate a unique selector
instance per component.createSelectorCreator
A: Yes! Reselect is now written in TS itself, so they should Just Work™.
Type instantiation is excessively deep and possibly infinite
A: This can often occur with deeply recursive types, which occur in this library. Please see this comment for a discussion of the problem, as relating to nested selectors.
A: Try these helper functions courtesy of MattSPalmer
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.
Chrome extension and companion lib for debugging selectors.
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.