A library for creating memoized "selector" functions. Commonly used with Redux, but usable with any plain JS immutable data as well.
Selectors can compute derived data, allowing Redux to store the minimal possible state.
Selectors are efficient. A selector is not recomputed unless one of its arguments changes.
Selectors are composable. They can be used as input to other selectors.
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.
Installation
Redux Toolkit
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'
Standalone
For standalone usage, install the reselect package:
npm install reselect
yarn add reselect
Basic Usage
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:
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 .
constselectValue=createSelector(state=>state.values.value1,state=>state.values.value2,(value1,value2)=>value1+value2)// You can also pass an array of selectorsconstselectTotal=createSelector([state=>state.values.value1,state=>state.values.value2],(value1,value2)=>value1+value2)// Selector behavior can be customizedconstcustomizedSelector=createSelector(state=>state.a,state=>state.b,(a,b)=>a+b,{// New in 4.1: Pass options through to the built-in `defaultMemoize` functionmemoizeOptions: {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:
constselectItemsByCategory=createSelector([// Usual first input - extract value from `state`state=>state.items,// Take the second arg, `category`, and forward to the output selector(state,category)=>category],// Output selector gets (`items, category)` as args(items,category)=>items.filter(item=>item.category===category))
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:
equalityCheck: used to compare the individual arguments of the provided calculation function
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 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 internally
The 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(memoize, ...memoizeOptions)
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:
constcustomSelectorCreator=createSelectorCreator(customMemoize,// function to be used to memoize resultFuncoption1,// option1 will be passed as second argument to customMemoizeoption2,// option2 will be passed as third argument to customMemoizeoption3// option3 will be passed as fourth argument to customMemoize)constcustomSelector=customSelectorCreator(input1,input2,resultFunc// resultFunc will be passed as first argument to customMemoize)
Internally customSelector calls the memoize function as follows:
customMemoize(resultFunc,option1,option2,option3)
Here are some examples of how you might use createSelectorCreator:
Customize equalityCheck for defaultMemoize
import{createSelectorCreator,defaultMemoize}from'reselect'importisEqualfrom'lodash.isequal'// create a "selector creator" that uses lodash.isequal instead of ===constcreateDeepEqualSelector=createSelectorCreator(defaultMemoize,isEqual)// use the new "selector creator" to create a selectorconstselectSum=createDeepEqualSelector(state=>state.values.filter(val=>val<5),values=>values.reduce((acc,val)=>acc+val,0))
Use memoize function from Lodash for an unbounded cache
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:
constselectA=state=>state.aconstselectB=state=>state.b// The result function in the following selector// is simply building an object from the input selectorsconststructuredSelector=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.
constselectA=state=>state.aconstselectB=state=>state.bconststructuredSelector=createStructuredSelector({x: selectA,y: selectB})constresult=structuredSelector({a: 1,b: 2})// will produce { x: 1, y: 2 }
Q: Why isn’t my selector recomputing when the input state changes?
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:
The following state update function will not work with selectIsFirstTodoComplete:
exportdefaultfunctiontodos(state=initialState,action){switch(action.type){caseCOMPLETE_ALL:
constareAllMarked=state.every(todo=>todo.completed)// BAD: mutating an existing objectreturnstate.map(todo=>{todo.completed=!areAllMarkedreturntodo})default:
returnstate}}
The following state update function will work with selectIsFirstTodoComplete:
exportdefaultfunctiontodos(state=initialState,action){switch(action.type){caseCOMPLETE_ALL:
constareAllMarked=state.every(todo=>todo.completed)// GOOD: returning a new object each time with Object.assignreturnstate.map(todo=>Object.assign({},todo,{completed: !areAllMarked}))default:
returnstate}}
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.
Q: Why is my selector recomputing when the input state stays the same?
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.
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.
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:
请发表评论