Re-reselect
re-reselect
is a lightweight wrapper around 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.
Useful to:
- retain selector's cache when sequentially called with one/few different arguments (example)
- join similar selectors into one
- share selectors with props across multiple components (see reselect example and re-reselect solution)
- instantiate selectors on runtime
import createCachedSelector from 're-reselect';
const selectorA = state => state.a;
const selectorB = (state, itemName) => state.items[itemName];
const cachedSelector = createCachedSelector(
selectorA,
selectorB,
(A, B) => expensiveComputation(A, B)
)(
(state, itemName) => itemName
);
const fooResult = cachedSelector(state, 'foo');
const barResult = cachedSelector(state, 'bar');
const fooResultAgain = cachedSelector(state, 'foo');
Table of contents
Installation
npm install reselect
npm install re-reselect
Why? + example
I found myself wrapping a library of data elaboration (quite heavy stuff) with reselect selectors (getPieceOfData
in the example).
On each store update, I had to repeatedly call the selector in order to retrieve all the pieces of data needed by my UI. Like this:
getPieceOfData(state, itemId, 'dataA');
getPieceOfData(state, itemId, 'dataB');
getPieceOfData(state, itemId, 'dataC');
What happens, here? getPieceOfData
selector cache is invalidated on each call because of the different 3rd 'dataX'
argument.
Re-reselect solution
re-reselect
selectors keep a cache of reselect
selectors and store/retrieve them by cacheKey
.
cacheKey
is by default a string
or number
but can be anything depending on the chosen cache strategy (see cacheObject
option).
cacheKey
is the output of resolverFunction
, declared at selector initialization.
resolverFunction
is a custom function which:
- takes the same arguments as the final selector (in the example:
state
, itemId
, 'dataX'
) - returns a
cacheKey
.
Note that the same reselect
selector instance stored in cache will be used for computing data for the same cacheKey
(1:1).
Back to the example, re-reselect
retrieves 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
):
const getPieceOfData = createCachedSelector(
state => state,
(state, itemId) => itemId,
(state, itemId, dataType) => dataType,
(state, itemId, dataType) => expensiveComputation(state, itemId, dataType)
)(
(state, itemId, dataType) => dataType
);
createCachedSelector
returns a selector with the same signature as a normal reselect
selector.
But now, each time the selector is called, the following happens behind the scenes:
- Evaluate the
cacheKey
for current call by executing resolverFunction
- Retrieve from cache the
reselect
selector stored under the given cacheKey
- Return found selector or create a new one if no selector was found
- Call returned selector with provided arguments
Re-reselect stays completely optional and consumes your installed reselect module (reselect
is declared as peer dependency).
Other viable solutions
1- Declare a different selector for each different call
Easy, but doesn't scale. See "join similar selectors" example.
2- Declare a makeGetPieceOfData
selector factory as explained in Reselect docs
Fine, but has a few downsides:
- Bloats your code by exposing both
get
selectors and makeGet
selector factories - Needs to import/call selector factory instead of directly using 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! :-) It's quite verbose (since has to be repeated for each selector), that's why re-reselect is here.
Examples
FAQ
How do I wrap my existing selector with re-reselect?
Given your reselect
selectors:
import {createSelector} from 'reselect';
export const getMyData = createSelector(
selectorA,
selectorB,
selectorC,
(A, B, C) => doSomethingWith(A, B, C)
);
...add resolverFunction
in the second function call:
import createCachedSelector from 're-reselect';
export const getMyData = createCachedSelector(
selectorA,
selectorB,
selectorC,
(A, B, C) => doSomethingWith(A, B, C)
)(
(state, arg1, arg2) => arg2
);
VoilΓ , getMyData
is ready for use!
const myData = getMyData(state, 'foo', 'bar');
How do I use multiple inputs to set the cacheKey?
cacheKey
is the return value of resolverFunction
.
resolverFunction
receives the same arguments of your inputSelectors
and (by default) must return a string
or number
.
A few good examples and a bonus:
createCachedSelector(
)(
(state, arg1, arg2, arg3) => arg3
)
createCachedSelector(
)(
(state, arg1, arg2, arg3) => `${arg1}:${arg3}`
)
createCachedSelector(
)(
(state, props) => `${props.a}:${props.b}`
)
How do I limit the cache size?
Use a cacheObject
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 shows how re-reselect
would solve the scenario described in reselect docs.
How do I test a re-reselect selector?
Just like a normal reselect selector! Read more here.
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 like:
resultFunc
recomputations
resetRecomputations
import createCachedSelector from 're-reselect';
export const getMyData = createCachedSelector(
selectorA,
selectorB,
(A, B) => doSomethingWith(A, B)
)(
(state, arg1) => arg1
);
const myFooData = getMyData(state, 'foo');
const myBarData = getMyData(state, 'bar');
const myFooDataSelector = getMyData.getMatchingSelector(state, 'foo');
const myBarDataSelector = getMyData.getMatchingSelector(state, 'bar');
myFooDataSelector.recomputations();
myFooDataSelector.resetRecomputations();
API
Re-reselect
exposes its cached selector creator as default export.
import reReselect from 're-reselect';
import createCachedSelector from 're-reselect';
reReselect([reselect's createSelector arguments])(resolverFunction, { cacheObject, selectorCreator })
Re-reselect accepts reselect's original createSelector
arguments and returns a new function which accepts 2 arguments:
resolverFunction
options { cacheObject, selectorCreator }
(optional)
resolverFunction
resolverFunction
is 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 resolverFunction
idea comes from Lodash's .memoize.
options.cacheObject
An optional custom strategy object to handle the caching behaviour.
Default cache: FlatObjectCache
.
re-reselect
provides 6 ready to use cache object constructors:
import createCachedSelector, {LruObjectCache, LruMapCache} from 're-reselect';
createCachedSelector(
)(
resolverFunction,
{
cacheObject: new LruObjectCache({cacheSize: 5}),
}
);
[*]ObjectCache strategy objects treat cacheKey
of type number
like strings, since they are used as arguments of JS objects.
[*]MapCache strategy objects needs a Map objects polyfill in order to use them on non-supporting browsers.
Custom cache strategy object
You can provide any kind of cache strategy. Declare a JS object adhering to the following interface:
interface ICacheObject {
set(key: any, selectorFn: any): void;
get(key: any): any;
remove(key: any): void;
clear(): void;
isValidCacheKey?(key: any): boolean;
}
options.selectorCreator
An optional function describing a custom selectors. By default it uses reselect
's createSelector
.
Returns
(Function): a reReselectInstance
selector ready to be used like a normal reselect selector.
reReselectInstance(selectorArguments)
Retrieve data for given arguments.
The followings are advanced methods and you won't need them for basic usage!
reReselectInstance.getMatchingSelector(selectorArguments)
Retrieve the selector responding to the given arguments.
reReselectInstance.removeMatchingSelector(selectorArguments)
Remove from the cache the selector responding to the given arguments.
reReselectInstance.clearCache()
Clear whole reReselectInstance
cache.
reReselectInstance.resultFunc
Get resultFunc
for easily test composed selectors.
Todo's
- Flow type definitions?
- Improve TS tests readability
- More examples
Contributors
Thanks to you all (emoji key):