Skip to content

v5.0.0-alpha.1

Pre-release
Pre-release
Compare
Choose a tag to compare
@markerikson markerikson released this 10 May 04:10
· 470 commits to master since this release

This alpha release adds two experimental new memoizers with different capabilities and tradeoffs.

npm i reselect@alpha

yarn add reselect@alpha

See the release notes for v5.0.0-alpha.0 for details on previous ESM/CJS build compat changes.

Changelog

New Experimental autotrack and weakmap Memoizers

Reselect has always allowed swapping out the function memoizer used inside of createSelector. Reselect's existing defaultMemoize memoizer is based on shallow equality checks for arguments. This is simple and fast, but also has limitations.

The most common limitation with defaultMemoize and shallow equality checks is that it can produce "false positive" recalculations. A classic example of this would be a selector that extracts an array of todo IDs:

const selectTodoIds = createSelector(
  (state: RootState) => state.todos,
  (todos) => todos.map(t => t.id)
)

If you dispatch a todoToggled() action that flips state.todos[3].completed, that will produce a new todo object at index 3 and a new todos array, because it's an immutable update. However, selectTodoIds will see that todos is a new reference and recalculate the result, even though none of the todo.id fields have changed. This creates a new IDs array that is shallow-equal to the last one. This is both a waste of computation time, and a new result reference that could cause a component to re-render even though the array hasn't conceptually changed.

createSelector has also always defaulted to a cache size of 1. With Reselect 4.1, we added a maxSize option to defaultMemoize, but this requires a known fixed cache size value at creation time. It's hard to estimate how many cache entries you might need in the future (will my list have 10 items? 100? 1000?).

This release includes two new experimental memoizers that have differing tradeoffs, with the goal of addressing these issues in different ways.

For now, both of these can be used by calling createSelectorCreator and generating a customized version of createSelector:

import { createSelectorCreator, autotrackMemoize, weakmapMemoize } from 'reselect'

const createSelectorAutotrack = createSelectorCreator(autotrackMemoize)
const createSelectorWeakmap = createSelectorCreator(weakmapMemoize)

In future 5.0-alpha releases, we'd like to investigate passing these directly to createSelector() calls.

autotrackMemoize

autotrackMemoize uses an "auto-tracking" approach inspired by the work of the Ember Glimmer team. It uses a Proxy to wrap arguments and track accesses to nested fields in your selector on first read. Later, when the selector is called with new arguments, it identifies which accessed fields have changed and only recalculates the result if one or more of those accessed fields have changed.

This allows it to be more precise than the shallow equality checks in defaultMemoize. In fact, with that exact same selectTodoIds code above, a selector that uses autotrackMemoize will not recalculate if you flip a todo.completed field, because it can see that you only accessed the todo.id fields.

This memoizer is directly based on the code and concepts from these articles and examples:

Design Tradeoffs for autotrackMemoize
  • It only has a cache size of 1
  • It is slower than defaultMemoize, because it has to do more work. (How much slower is dependent on the number of accessed fields in a selector, number of calls, frequency of input changes, etc)
  • It can have some unexpected behavior. Because it tracks nested field accesses, cases where you don't access a field will not recalculate properly. For example, a badly-written selector like createSelector(state => state.todos, todos => todos) that just immediately returns the extracted value will never update, because it doesn't see any field accesses to check. (You shouldn't write selectors like that to begin with :) But we've seen them in the wild.)
  • It is likely to avoid excess calculations and recalculate fewer times than defaultMemoize will, which may also result in fewer component re-renders
Use Cases for autotrackMemoize

autotrackMemoize is likely best used for cases where you need to access specific nested fields in data, and avoid recalculating if other fields in the same data objects are immutably updated.

weakmapMemoize

defaultMemoize has to be explicitly configured to have a cache size larger than 1, and uses an LRU cache internally.

weakmapMemoize creates a tree of WeakMap-based cache nodes based on the identity of the arguments it's been called with (in this case, the extracted values from your input functions). This allows weakmapMemoize to have an effectively infinite cache size. Cache results will be kept in memory as long as references to the arguments still exist, and then cleared out as the arguments are garbage-collected.

This memoizer is directly based on code from the React codebase:

Design Tradeoffs for weakmapMemoize
  • There's currently no way to alter the argument comparisons - they're based on strict reference equality
  • It's roughly the same speed as defaultMemoize, although likely a fraction slower
  • It has an effectively infinite cache size, but you have no control over how long values are kept in cache as it's based on garbage collection and WeakMaps
Use Cases for weakmapMemoize

This memoizer is likely best used for cases where you need to call the same selector instance with many different arguments, such as a single selector instance that is used in a list item component and called with item IDs like useSelector(state => selectSomeData(state, props.category)).

Argument Memoization Uses defaultMemoize

Back in PR #297, the outer argument memoization was changed to use the same provided memoization function as the inner extracted values memoization. For performance reasons, we've flipped this back to use defaultMemoize and shallow equality checks for the outer argument memoization. In most cases this should have no change at all for end users, because the memoizer is rarely overridden anyway.

We'd like to investigate allowing customization of both arguments and extracted values memoizers in a later 5.0-alpha release

What's Changed

Full Changelog: v5.0.0-alpha.0...v5.0.0-alpha.1