Skip to content
Enhance Reselect selectors with deeper memoization and cache management.
Branch: master
Clone or download
Latest commit f7f8175 Feb 10, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.github
examples Added the word 'the' because I read it out load a few times and it ma… Oct 11, 2018
jest Setup tests for compiled bundles Aug 22, 2018
src Rename resolver as keySelector Feb 10, 2019
typescript_test dependencies added in typing, supported heterogeneous selectors and a… Jan 31, 2019
.all-contributorsrc
.babelrc.js Use loose mode Oct 18, 2018
.editorconfig Add .editorconfig Feb 25, 2017
.gitignore
.npmrc
.prettierignore Add missing .prettierignore file Aug 22, 2018
.prettierrc
.travis.yml
CHANGELOG.md
CONTRIBUTING.md Update docs Jan 31, 2019
LICENSE.md
README.md
package-lock.json 3.0.0 Feb 10, 2019
package.json 3.0.0 Feb 10, 2019
rollup.config.js

README.md

Re-reselect

Build status Npm version Npm downloads Test coverage report

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.

Reselect and re-reselect

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 component instances (see reselect example and re-reselect solution)
  • instantiate selectors on runtime
import createCachedSelector from 're-reselect';

// Normal reselect routine: declare "inputSelectors" and "resultFunc"
const selectorA = state => state.a;
const selectorB = (state, itemName) => state.items[itemName];

const cachedSelector = createCachedSelector(
  // inputSelectors
  selectorA,
  selectorB,

  // resultFunc
  (A, B) => expensiveComputation(A, B)
)(
  // keySelector
  // Instruct re-reselect to use "itemName" as cacheKey
  (state, itemName) => itemName
);

// Use the cached selector like a normal selector:
const fooResult = cachedSelector(state, 'foo');
const barResult = cachedSelector(state, 'bar');

// 2 reselect selectors were created, called and cached behind the scenes

const fooResultAgain = cachedSelector(state, 'foo');

// fooResult === fooResultAgain
// Cache was not invalidated by calling "cachedSelector(state, 'bar')"
// "expensiveComputation" totally called twice

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 keySelector, declared at selector initialization.

keySelector 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 // Use dataType as cacheKey
);

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:

  1. Evaluate the cacheKey for current call by executing keySelector
  2. Retrieve from cache the reselect 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

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

The solution suggested in Reselect docs is fine, but it 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 keySelector 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 // Use arg2 as cacheKey
);

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 keySelector.

keySelector receives the same arguments of your inputSelectors and (by default) must return a string or number.

A few good examples and a bonus:

// 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 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!

re-reselect selectors expose the same reselect testing methods:

  • dependencies
  • resultFunc
  • recomputations
  • resetRecomputations

Read more about testing selectors on reselect docs.

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.

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();

API

Re-reselect exposes its cached selector creator as default export.

import reReselect from 're-reselect';
// or:
import createCachedSelector from 're-reselect';

reReselect([reselect's createSelector arguments])(keySelector, { cacheObject, selectorCreator })

Re-reselect accepts reselect's original createSelector arguments and returns a new function which accepts 2 arguments:

  • keySelector
  • options { cacheObject, selectorCreator } (optional)

keySelector

keySelector 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 keySelector 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:

name accepted cacheKey type storage
FlatObjectCache number string flat unlimited JS object
FifoObjectCache number string first in first out JS object
LruObjectCache number string least recently used JS object
FlatMapCache any flat unlimited Map object
FifoMapCache any first in first out Map object
LruMapCache any least recently used Map object
import createCachedSelector, {LruObjectCache, LruMapCache} from 're-reselect';

createCachedSelector(
  // ...
)(
  keySelector,
  {
    cacheObject: new LruObjectCache({cacheSize: 5}),
    // or:
    // cacheObject: new LruMapCache({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; // optional
}

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.cache

Get cacheObject instance being used by the selector (for advanced caching operations like this).

reReselectInstance.clearCache()

Clear whole reReselectInstance cache.

reReselectInstance.dependencies

Get an array containing the provided inputSelectors. Refer to relevant discussion on Reselect repo.

reReselectInstance.resultFunc

Get resultFunc for easily testing composed selectors.

reReselectInstance.recomputations()

Return the number of times the selector's result function has been recomputed.

reReselectInstance.resetRecomputations()

Reset recomputations count.

Todo's

  • Improve TS tests readability
  • More examples

Contributors

Thanks to you all (emoji key):


Andrea Carraro

💻 📖 🚇 ⚠️ 👀

Stepan Burguchev

💻 👀 ⚠️

Mitch Robb

💻 ⚠️

Stephane Rufer

💻 ⚠️

Tracy Mullen

💻 ⚠️

Sushain Cherivirala

💻

Steve Mao

📖

Gaurav Lahoti

🐛

Lon

🐛

bratushka

💻

Anders D. Johnson

📖

Július Retzer

📖

Maarten Schumacher

🤔

Alexander Jarvis

🤔

Gregg B

💡

Ian Obermiller

👀

Kanitkorn Sujautra

📖

Brian Kraus

📖

Mateusz Burzyński

💻 🚇

el-dav

🐛

Sergei Grishchenko

💻 ⚠️

Augustin Riedinger

🤔
You can’t perform that action at this time.