Enhance Reselect selectors with deeper memoization and cache management
npm install re-reselect[![Build status][ci-badge]][ci]
[![Npm version][npm-version-badge]][npm]
[![Npm downloads][npm-downloads-badge]][npm]
[![Test coverage report][coveralls-badge]][coveralls]
From v5, reselect provides the ability to natively implement custom memoization/caching solutions via createSelector options. Most of the features re-reselect used to enable should be now natively available in reselect. re-reselect will try to support reselect v5+ for backward compatibility reasons.
re-reselect is a lightweight wrapper around [Reselect][reselect] meant to enhance selectors with deeper memoization and cache management.
Switching between different arguments using standard reselect selectors causes cache invalidation since default reselect cache has a limit of one.
re-reselect forwards different calls to different reselect selectors stored in cache, so that computed/memoized values are retained.
re-reselect selectors work as normal reselect selectors but they are able to determine when creating a new selector or querying a cached one on the fly, depending on the supplied arguments.
![Reselect and re-reselect][reselect-and-re-reselect-sketch]
Useful to:
- Retain selector's cache when sequentially called with one/few different arguments ([example][example-1])
- Join similar selectors into one
- Share selectors with props across multiple component instances (see [reselect example][reselect-sharing-selectors] and [re-reselect solution][example-2])
- Instantiate selectors on runtime
- Enhance reselect with [custom caching strategies][cache-objects-docs]
``js
import {createCachedSelector} from 're-reselect';
// Normal reselect routine: declare "inputSelectors" and "resultFunc"
const getUsers = state => state.users;
const getLibraryId = (state, libraryName) => state.libraries[libraryName].id;
const getUsersByLibrary = createCachedSelector(
// inputSelectors
getUsers,
getLibraryId,
// resultFunc
(users, libraryId) => expensiveComputation(users, libraryId),
)(
// re-reselect keySelector (receives selectors' arguments)
// Use "libraryName" as cacheKey
(_state_, libraryName) => libraryName
);
// Cached selectors behave like normal selectors:
// 2 reselect selectors are created, called and cached
const reactUsers = getUsersByLibrary(state, 'react');
const vueUsers = getUsersByLibrary(state, 'vue');
// This 3rd call hits the cache
const reactUsersAgain = getUsersByLibrary(state, 'react');
// reactUsers === reactUsersAgain
// "expensiveComputation" called twice in total
`
- Installation
- Why? + example
- re-reselect solution
- Other viable solutions
- Examples
- FAQ
- API
- createCachedSelector
- createStructuredCachedSelector
- keySelector
- options
- [selector instance][selector-instance-docs]
- About re-reselect
- Todo's
- Contributors
`console`
npm install reselect
npm install re-reselect
Let's say getData is a reselect selector.
`js`
getData(state, itemId, 'dataA');
getData(state, itemId, 'dataB');
getData(state, itemId, 'dataA');
The 3rd argument invalidates reselect cache on each call, forcing getData to re-evaluate and return a new value.
re-reselect selectors keep a cache of reselect selectors stored by cacheKey.
cacheKey is the return value of the keySelector function. It's by default a string or number but it can be anything depending on the chosen cache strategy (see [cache objects docs][cache-objects-docs]).
keySelector is a custom function which:
- takes the same arguments as the selector itself (in the example: state, itemId, dataType)cacheKey
- returns a
A unique persisting reselect selector instance stored in cache is used to compute data for a given cacheKey (1:1).
Back to the example, we might setup re-reselect to retrieve data by querying one of the cached selectors using the 3rd argument as cacheKey, allowing cache invalidation only when state or itemId change (but not dataType):
`js`
const getData = createCachedSelector(
state => state,
(state, itemId) => itemId,
(state, itemId, dataType) => dataType,
(state, itemId, dataType) => expensiveComputation(state, itemId, dataType)
)(
(state, itemId, dataType) => dataType // Use dataType as cacheKey
);
Replacing a selector with a cached selector is invisible to the consuming application since the API is the same.
When a cached selector is called, the following happens behind the scenes:
1. Evaluate the cacheKey for the current call by executing keySelectorreselect
2. Retrieve from cache the selector stored under the given cacheKey
3. Return found selector or create a new one if no selector was found
4. Call returned selector with provided arguments
#### 1- Declare a different selector for each different call
Easy, but doesn't scale. See ["join similar selectors" example][example-1].
#### 2- Declare a makeGetPieceOfData selector factory as explained in Reselect docs
The solution suggested in [Reselect docs][reselect-sharing-selectors] is fine, but it has a few downsides:
- Bloats your code by exposing both get selectors and makeGet selector factories
- Needs to import/call the selector factory instead of directly using the selector
- Two different instances, given the same arguments, will individually store and recompute the same result (read this)
#### 3- Wrap your makeGetPieceOfData selector factory into a memoizer function and call the returning memoized selector
This is what re-reselect actually does. 😀
- [Join similar selectors][example-1]
- [Avoid selector factories][example-2]
- [Cache API calls][example-3]
- [Programmatic keySelector composition][example-4]
- [Usage with Selectorator][example-5]
How do I wrap my existing selector with re-reselect?
Given your reselect selectors:
`js
import {createSelector} from 'reselect';
export const getMyData = createSelector(
selectorA,
selectorB,
selectorC,
(A, B, C) => doSomethingWith(A, B, C)
);
`
...add keySelector in the second function call:
`js
import {createCachedSelector} from 're-reselect';
export const getMyData = createCachedSelector(
selectorA,
selectorB,
selectorC,
(A, B, C) => doSomethingWith(A, B, C)
)(
(state, arg1, arg2) => arg2 // Use arg2 as cacheKey
);
`
Voilà, getMyData is ready for use!
`js`
const myData = getMyData(state, 'foo', 'bar');
How do I use multiple inputs to set the cacheKey?
A few good examples and a bonus:
`js
// Basic usage: use a single argument as cacheKey
createCachedSelector(
// ...
)(
(state, arg1, arg2, arg3) => arg3
)
// Use multiple arguments and chain them into a string
createCachedSelector(
// ...
)(
(state, arg1, arg2, arg3) => ${arg1}:${arg3}
)
// Extract properties from an object
createCachedSelector(
// ...
)(
(state, props) => ${props.a}:${props.b}`
)
How do I limit the cache size?
Use a [cacheObject][cache-objects-docs] which provides that feature by supplying a cacheObject option.
You can also write your own cache strategy!
How to share a selector across multiple components while passing in props and retaining memoization?
[This example][example-2] shows how re-reselect would solve the scenario described in [reselect docs][reselect-sharing-selectors].
How do I test a re-reselect selector?
Like a normal reselect selector!
re-reselect selectors expose the same reselect testing methods:
- dependenciesresultFunc
- recomputations
- resetRecomputations
-
Read more about testing selectors on [reselect docs][reselect-test-selectors].
#### Testing reselect selectors stored in the cache
Each re-reselect selector exposes a getMatchingSelector method which returns the underlying matching selector instance for the given arguments, instead of the result.
getMatchingSelector expects the same arguments as a normal selector call BUT returns the instance of the cached selector itself.
Once you get a selector instance you can call [its public methods][reselect-selectors-methods].
`js
import {createCachedSelector} from 're-reselect';
export const getMyData = createCachedSelector(selectorA, selectorB, (A, B) =>
doSomethingWith(A, B)
)(
(state, arg1) => arg1 // cacheKey
);
// Call your selector
const myFooData = getMyData(state, 'foo');
const myBarData = getMyData(state, 'bar');
// Call getMatchingSelector method to retrieve underlying reselect selectors
// which generated "myFooData" and "myBarData" results
const myFooDataSelector = getMyData.getMatchingSelector(state, 'foo');
const myBarDataSelector = getMyData.getMatchingSelector(state, 'bar');
// Call reselect's selectors methods
myFooDataSelector.recomputations();
myFooDataSelector.resetRecomputations();
`
`js
import {createCachedSelector} from 're-reselect';
createCachedSelector(
// ...reselect's createSelector arguments`
)(
keySelector | { options }
)
Takes the same arguments as reselect's [createSelector][reselect-create-selector] and returns a new function which accepts a keySelector or an options object.
Returns a [selector instance][selector-instance-docs].
`js
import {createStructuredCachedSelector} from 're-reselect';
createStructuredCachedSelector(
// ...reselect's createStructuredSelector arguments`
)(
keySelector | { options }
)
Takes the same arguments as reselect's [createStructuredSelector][reselect-create-structured-selector] and returns a new function which accepts a keySelector or an options object.
Returns a [selector instance][selector-instance-docs].
A custom function receiving the same arguments as your selectors (and inputSelectors) and returning a cacheKey.
cacheKey is by default a string or number but can be anything depending on the chosen cache strategy (see cacheObject option).
The keySelector idea comes from [Lodash's .memoize resolver][lodash-memoize].
#### keySelector
Type: functionundefined
Default:
The keySelector used by the cached selector.
#### cacheObject
Type: objectFlatObjectCache
Default: [][cache-objects-docs]
An optional custom cache strategy object to handle the caching behaviour. Read more about [re-reselect's custom cache here][cache-objects-docs].
#### keySelectorCreator
Type: functionundefined
Default:
An optional function with the following signature returning the keySelector used by the cached selector.
`typescript`
type keySelectorCreator = (selectorInputs: {
inputSelectors: InputSelector[];
resultFunc: ResultFunc;
keySelector: KeySelector;
}) => KeySelector;
This allows the ability to dynamically generate keySelectors on runtime based on provided inputSelectors/resultFunc supporting key selectors composition. It overrides any provided keySelector.
See [programmatic keySelector composition][example-4] example.
#### selectorCreator
Type: functionreselect
Default: 's [createSelector][reselect-create-selector]
An optional function describing a [custom version of createSelector][reselect-create-selector-creator].
createCachedSelector and createStructuredCachedSelector return a selector instance which extends the API of a standard reselect selector.
> The followings are advanced methods and you won't need them for basic usage!
#### selector.getMatchingSelector(selectorArguments)
Retrieve the selector responding to the given arguments.
#### selector.removeMatchingSelector(selectorArguments)
Remove from the cache the selector responding to the given arguments.
#### selector.cache
Get the cacheObject instance being used by the selector (for advanced caching operations like this).
#### selector.clearCache()
Clear whole selector cache.
#### selector.dependencies
Get an array containing the provided inputSelectors. Refer to relevant discussion on [Reselect repo][reselect-test-selectors-dependencies].
#### selector.resultFunc
Get resultFunc for easily [testing composed selectors][reselect-test-selectors].
#### selector.recomputations()
Return the number of times the selector's result function has been recomputed.
#### selector.resetRecomputations()
Reset recomputations count.
#### selector.keySelector
Get keySelector for utility compositions or testing.
- re-reselect your whole redux state
- Understanding reselect and re-reselect
- React re-reselect: Better memoization and cache management
- Advanced Redux patterns: selectors
- Be selective with your state
- A swift developer’s React Native experience
- 5 key Redux libraries to improve code reuse
- Rematch's docs
- Redux re-reselect playground
- Improve tests readability
- Port to native TS based on reselect v5 approach
- Find out whether re-reselect should be deprecated in favour of reselect` memoization/cache options
Thanks to you all ([emoji key][docs-all-contributors]):
[reselect]: https://github.com/reactjs/reselect
[reselect-sharing-selectors]: https://github.com/reduxjs/reselect/tree/v4.0.0#sharing-selectors-with-props-across-multiple-component-instances
[reselect-test-selectors]: https://github.com/reactjs/reselect/tree/v4.0.0#q-how-do-i-test-a-selector
[reselect-test-selectors-dependencies]: https://github.com/reduxjs/reselect/issues/76#issuecomment-299194186
[reselect-selectors-methods]: https://github.com/reduxjs/reselect/blob/v4.0.0/src/index.js#L81
[reselect-create-selector]: https://github.com/reactjs/reselect/tree/v4.0.0#createselectorinputselectors--inputselectors-resultfunc
[reselect-create-structured-selector]: https://github.com/reduxjs/reselect/tree/v4.0.0#createstructuredselectorinputselectors-selectorcreator--createselector
[reselect-create-selector-creator]: https://github.com/reactjs/reselect/tree/v4.0.0#createselectorcreatormemoize-memoizeoptions
[lodash-memoize]: https://lodash.com/docs/4.17.4#memoize
[ci-badge]: https://github.com/toomuchdesign/re-reselect/actions/workflows/ci.yml/badge.svg
[ci]: https://github.com/toomuchdesign/re-reselect/actions/workflows/ci.yml
[coveralls-badge]: https://coveralls.io/repos/github/toomuchdesign/re-reselect/badge.svg?branch=master
[coveralls]: https://coveralls.io/github/toomuchdesign/re-reselect?branch=master
[npm]: https://www.npmjs.com/package/re-reselect
[npm-version-badge]: https://img.shields.io/npm/v/re-reselect.svg
[npm-downloads-badge]: https://img.shields.io/npm/dm/re-reselect.svg
[reselect-and-re-reselect-sketch]: examples/reselect-and-re-reselect.png?raw=true
[example-1]: examples/1-join-selectors.md
[example-2]: examples/2-avoid-selector-factories.md
[example-3]: examples/3-cache-api-calls.md
[example-4]: examples/4-programmatic-keyselector-composition.md
[example-5]: examples/5-selectorator.md
[selector-instance-docs]: #re-reselect-selector-instance
[cache-objects-docs]: src/cache#readme
[docs-all-contributors]: https://allcontributors.org/docs/en/emoji-key