Re-reselect
Improve Reselect selectors performance/usage on a few edge cases, by initializing selectors on runtime, using a memoized factory.
Re-reselect returns a reselect-like selector, which is able to determine internally when querying a new selector instance or a cached one on the fly, depending on the supplied arguments.
Useful to reduce selectors recalculation when:
- a selector is sequentially called with one/few different arguments
- a selector is imported by different modules at the same time
...or to:
import reselect from 'reselect';
import createCachedSelector from 're-reselect';
const selectorA = state => state.a;
const selectorB = state => state.b;
const cachedSelector = createCachedSelector(
selectorA,
selectorB,
(state, someArg) => someArg,
(A, B, someArg) => expensiveComputation(A, B, someArg),
)(
(state, someArg) => someArg,
);
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 my self 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', otherArg);
getPieceOfData(state, itemId, 'dataB', otherArg);
getPieceOfData(state, itemId, 'dataC', otherArg);
What happens, here? getPieceOfData
selector cache is invalidated on each call because of the changing 3rd dataX
argument.
Re-reselect solution
createCachedSelector
keeps a private collection of selectors and store them by key
.
key
is the output of the resolver
function, declared at selector initialization.
resolver
is a custom function which receives the same arguments as the final selector (in the example: state
, itemId
, 'dataX'
, otherArgs
) and returns a string
or number
.
That said, I was able to configure re-reselect
to retrieve my data by querying a set of cached selectors using the 3rd argument as cache key:
const getPieceOfData = createCachedSelector(
state => state,
(state, itemId) => itemId
(state, itemId, dataType) => dataType
(state, itemId, dataType, otherArg) => otherArg
(state, itemId, dataType, otherArg) => expensiveComputation(state, itemId, dataType, otherArg),
)(
(state, itemId, dataType) => dataType,
);
The final result is a normal selector taking the same arguments as before.
But now, each time the selector is called, the following happens behind the scenes:
- Run
resolver
function and get its result (the cache key) - Look for a matching key from the cache
- Return a cached selector or create a new one if no matching key is found in cache
- Call selector with provided arguments
Re-reselect stays completely optional and uses your installed reselect library under the hoods (reselect is declared as a peer dependency).
Furthermore you can use any custom selector (see API).
Other viable solutions
1- Declare a different selector for each different call
Easy but doesn't scale.
2- Declare a makeGetPieceOfData
selector factory as explained in Reselect docs
Fine, but has 2 downsides:
- Bloat your selectors module by exposing both
get
selectors and makeGet
selector factories - Two different selector instances given the same arguments will individually recompute and store 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 should be repeated for each selector), that's why re-reselect is here.
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),
);
...it becomes:
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!
let myData = getMyData(state, 'foo', 'bar');
How do I use multiple inputs to set the cache key?
The cache key is defined by the output of the resolverFunction
.
resolverFunction
is a function which receives the same arguments of your inputSelectors
and 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 the cacheObject
option.
How to share a selector across multiple components while passing in props and retaining memoization?
This example is how re-reselect
would solve the scenario described in Reselect docs.
We can directly declare getVisibleTodos
selector. Since re-reselect
handles selectors instantiation transparently, there is no need to declare a makeGetVisibleTodos
factory.
selectors/todoSelectors.js
import createCachedSelector from 're-reselect';
const getVisibilityFilter = (state, props) =>
state.todoLists[props.listId].visibilityFilter
const getTodos = (state, props) =>
state.todoLists[props.listId].todos
const getVisibleTodos = createCachedSelector(
[ getVisibilityFilter, getTodos ],
(visibilityFilter, todos) => {
switch (visibilityFilter) {
case 'SHOW_COMPLETED':
return todos.filter(todo => todo.completed)
case 'SHOW_ACTIVE':
return todos.filter(todo => !todo.completed)
default:
return todos
}
}
)(
(state, props) => props.listId,
);
export default getVisibleTodos;
containers/VisibleTodoList.js
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
import { getVisibleTodos } from '../selectors'
const mapStateToProps = (state, props) => {
return {
todos: getVisibleTodos(state, props)
}
}
How do I test a re-reselect selector?
Just like a normal reselect selector! Read more here.
Each re-reselect cached 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';
reReselect([reselect's createSelector arguments])(resolverFunction, { cacheObject, selectorCreator })
Re-reselect accepts your original selector creator arguments and returns a new function which accepts 2 arguments:
resolverFunction
options { cacheObject, selectorCreator }
(optional)
resolverFunction
resolverFunction
is a function which receives the same arguments of your selectors (and inputSelectors
) and must return a string or number. The result is used as cache key to store/retrieve selector instances.
Cache keys of type number
are treated like strings, since they are assigned to a JS object as arguments.
The resolver idea is inspired by Lodash's .memoize util.
options.cacheObject
An optional custom strategy object to handle the caching behaviour. It must adhere to the following interface:
interface ICacheObject {
set (key: string|number, selectorFn: Function): void;
get (key: string|number): Function;
remove (key: string|number): void;
clear (): void;
}
re-reselect
provides 3 ready to use cache object creators:
import createCachedSelector, { LruCacheObject, FifoCacheObject } from re-reselect;
createCachedSelector(
)(
resolverFunction,
{
cacheObject: new LruCacheObject({ cacheSize: 5 });
}
)
The default cache strategy, FlatCache
doesn't limit cache.
You can provide any kind of caching strategy. Just write your own. You can use the existing ones as starting point.
options.selectorCreator
selectorCreator
is 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 the selector responding to the given arguments from the cache.
reReselectInstance.clearCache()
Clear the whole reReselectInstance
cache.
reReselectInstance.resultFunc
Get resultFunc
for easily test composed selectors.
Todo's
- Flow type definitions?
- Improve TS tests readability
Contributors
Thanks to you all (emoji key):