Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

rememo

aduth338kMIT4.0.2TypeScript support: included

Memoized selectors for Redux and other immutable object derivation

redux, selector, selectors, memoize, memoization, cache

readme

Rememo

Memoized selectors for Redux and other immutable object derivation.

Usage

Rememo's default export is a function which accepts two arguments: the selector function whose return value is to be cached, and a second function which returns the reference or array of references upon which the selector's derivation depends. The return value is a new function which accepts the same arguments as the selector.

import createSelector from 'rememo';

const getTasksByCompletion = createSelector(
    // The expensive computation:
    (state, isComplete) =>
        state.todo.filter((task) => task.complete === isComplete),

    // The reference(s) upon which the computation depends:
    (state) => [state.todo]
);

// The selector will only calculate the return value once so long as the state
// `todo` reference remains the same
let completedTasks;
completedTasks = getTasksByCompletion(state, true); // Computed
completedTasks = getTasksByCompletion(state, true); // Returned from cache

Installation

Rememo is published as an npm package:

npm install rememo

Browser-ready versions are available from unpkg. The browser-ready version assigns itself on the global scope as window.rememo.

<script src="https://unpkg.com/rememo/dist/rememo.min.js"></script>
<script>
    var createSelector = window.rememo;

    // ...
</script>

API

Rememo's default export is a function:

createSelector(
    selector: (...args: any[]) => any,
    getDependants?: (...args: any[]) => any[],
): (...args: any[]) => any

The returned function is a memoized selector with the following signature:

memoizedSelector(source: object, ...args: any[]): any

It's expected that the first argument to the memoized function is the source from which the selector operates. It is ignored when considering whether the argument result has already been cached.

The memoized selector function includes two additional properties:

  • clear(): When invoked, resets memoization cache.
  • getDependants(source: Object, ...args: any[]): The dependants getter for the selector.

The getDependants property can be useful when creating selectors which compose other memoized selectors, in which case the dependants are the union of the two selectors' dependants:

const getTasksByCompletion = createSelector(
    (state, isComplete) =>
        state.todo.filter((task) => task.complete === isComplete),
    (state) => [state.todo]
);

const getTasksByCompletionForCurrentDate = createSelector(
    (state, isComplete) =>
        getTasksByCompletion(state, isComplete).filter(
            (task) => task.date === state.currentDate
        ),
    (state, isComplete) => [
        ...getTasksByCompletion.getDependants(state, isComplete),
        state.currentDate,
    ]
);

Motivation

While designed specifically for use with Redux, Rememo is a simple pattern for efficiently deriving values from any immutable data object. Rememo takes advantage of Redux's core principles of data normalization and immutability. While tracking normalized data in a Redux store is beneficial for eliminating redudancy and reducing overall memory storage, in doing so it sacrifices conveniences that would otherwise make for a pleasant developer experience. It's for this reason that a selector pattern can be desirable. A selector is nothing more than a function which receives the current state and optionally a set of arguments to be used in determining the calculated value.

For example, consider the following state structure to describe a to-do list application:

const state = {
    todo: [
        { text: 'Go to the gym', complete: true },
        { text: 'Try to spend time in the sunlight', complete: false },
        { text: 'Laundry must be done', complete: true },
    ],
};

If we wanted to filter tasks by completion, we could write a simple function:

function getTasksByCompletion(state, isComplete) {
    return state.todo.filter((task) => task.complete === isComplete);
}

This works well enough and requires no additional tools, but you'll observe that the filtering we perform on the set of to-do tasks could become costly if we were to have thousands of tasks. And this is just a simple example; real-world use cases could involve far more expensive computation. Add to this the very real likelihood that our application might call this function many times even when our to-do set has not changed.

Furthermore, when used in combination with React.PureComponent or react-redux's connect — which creates pure components by default — it is advisable to pass unchanging object and array references as props on subsequent renders. A selector which returns a new reference on each invocation (as occurs with Array#map or Array#filter), your component will needlessly render even if the underlying data does not change.

This is where Rememo comes in: a Rememo selector will cache the resulting value so long as the references upon which it depends have not changed. This works particularly well for immutable data structures, where we can perform a trivial strict equality comparison (===) to determine whether state has changed. Without guaranteed immutability, equality can only be known by deeply traversing the object structure, an operation which in many cases is far more costly than the original computation.

In our above example, we know the value of the function will only change if the set of to-do's has changed. It's in Rememo's second argument that we describe this dependency:

const getTasksByCompletion = createSelector(
    (state, isComplete) =>
        state.todo.filter((task) => task.complete === isComplete),
    (state) => [state.todo]
);

Now we can call getTasksByCompletion as many times as we want without needlessly wasting time filtering tasks when the todo set has not changed.

Testing

To simplify testing of memoized selectors, the function returned by createSelector includes a clear function:

const getTasksByCompletion = require('../selector');

// Test licecycle management varies by runner. This example uses Mocha.
beforeEach(() => {
    getTasksByCompletion.clear();
});

Alternatively, you can create separate references (exports) for your memoized and unmemoized selectors, then test only the unmemoized selector.

Refer to Rememo's own tests as an example.

FAQ

How does this differ from Reselect, another selector memoization library?

Reselect and Rememo largely share the same goals, but have slightly different implementation semantics. Reselect optimizes for function composition, requiring that you pass as arguments functions returning derived data of increasing specificity. Constrasting it to our to-do example above, with Reselect we would pass two arguments: a function which retrieves todo from the state object, and a second function which receives that set as an argument and performs the completeness filter. The distinction is not as obvious with a simple example like this one, and can be seen more clearly with examples in Reselect's README.

Rememo instead encourages you to consider the derivation first-and-foremost without requiring you to build up the individual dependencies ahead of time. This is especially convenient if your computation depends on many disparate state paths, or if you choose not to memoize all selectors and would rather opt-in to caching at your own judgment. Composing selectors is still straight-forward in Rememo if you subscribe to the convention of passing state always as the first argument, since this enables your selectors to call upon other each other passing the complete state object.

License

Copyright 2018-2022 Andrew Duthie

Released under the MIT License.

changelog

v4.0.2 (2022-10-04)

  • TypeScript: Fix issue preventing TypeScript types from being resolved

v4.0.1 (2022-06-15)

  • Bug Fix: Fix error when importing in CommonJS projects.

v4.0.0 (2021-07-11)

  • Breaking: Drop support for environments which don't have WeakMap. This should only affect Internet Explorer 10 and older.
  • New: TypeScript type definitions are now included.
  • Miscellaneous: The package is now implemented as a native ES module.

v3.0.0 (2018-02-17)

  • Breaking: getDependants (the second argument) must return an array. Per the below added feature, this has been done in an effort to to reduce developer burden in normalizing dependants reuse as arrays.
  • New: The created selector exposes getDependants function as a property. Refer to README.md for usage.

v2.4.1 (2018-02-17)

  • Improved: Minor size and performance optimization on cache arguments handling.

v2.4.0 (2018-02-11)

  • Improved: Now uses WeakMap when available and when possible to cache per set of dependants. This also results in improved cache hit rates for dependants derived from getter arguments.
  • Removed: options.maxSize is no longer supported. The options argument, if passed, is now simply ignored.

v2.3.4 (2018-01-25)

  • Fix: Correctly skips incorrect cached value return on mismatched argument length

v2.3.3 (2017-09-06)

  • Fix: Resolve infinite loop which can occur due to lingering references in recalling from previous cache

v2.3.2 (2017-08-30)

  • Fix: Resolve error which can occur in certain conditions with maxSize

v2.3.1 (2017-08-24)

  • Fix: Resolve infinite loop which can occur due to lingering references in recalling from previous cache

v2.3.0 (2017-08-08)

  • Improved: Significant performance optimizations by reimplementing cache as linked list stack. For more details and benchmarks, see sister project "memize" from which the implementation is derived.

v2.2.0 (2017-08-04)

  • Improved: Performance optimization on creating argument cache
  • Fix: Skip impossible condition when deciding to surface result to top of cache

v2.1.0 (2017-07-27)

  • Improved: Performance optimization on multiple subsequent selector calls with identical arguments
  • Fix: Use correct cache to determine cache update optimization

v2.0.0 (2017-07-27)

  • Breaking Change: The memoized function is no longer exposed. Calls to selector.memoizedSelector.clear should be updated to selector.clear.
  • New Feature: createSelector accepts an optional third argument to specify options, currently supporting maxSize (defaulting to Infinity)
  • Internal: Cache lookup and max size use an LRU (least recently used) policy to bias recent access, improving efficiency on subsequent calls with same arguments
  • Internal: Inline memoization with returned selector to optimize arguments handling

v1.2.0 (2017-07-24)

  • Internal: Drop moize dependency in favor of home-grown memoization solution, significantly reducing bundled size (10.2kb -> 0.5kb minified, 3.0kb -> 0.3kb minified + gzipped)
  • Internal: Add package-lock.json

v1.1.1 (2017-06-13)

  • Fix: Resolve an error in environments not supporting Promise, caused by defaults behavior in the underlying memoization library.

v1.1.0 (2017-06-08)

  • Improved: Object target is ignored in generating memoized function cache key. This can resolve issues where cache would be discarded if dependant references were the same but the target object reference changed.

v1.0.2 (2017-05-29)

  • Fix: Include dist in npm package (for unpkg availability)

v1.0.0 (2017-05-27)

  • Initial release