Skip to content

v4.1.0

Compare
Choose a tag to compare
@markerikson markerikson released this 26 Oct 02:29
· 618 commits to master since this release

This long-overdue release updates defaultMemoize to accept new options for cache size > 1 and a result equality check, updates createSelector to accept an options object containing options for the provided memoize function, makes major improvements to the TypeScript types (targeting TS 4.2+), converts the codebase to TS, improves some error messages, and adds memoizedResultFunc and lastResult to the fields attached to the selector,

This should be a drop-in update - the only expected backwards compatibility issues are with incorrect or very outdated TypeScript usage patterns.

Update: see https://github.com/reduxjs/reselect/releases/tag/v4.1.1 for fixes to several TS and other issues that were reported with the 4.1.0 release

npm i reselect@latest

yarn add reselect@latest

Changelog

New defaultMemoize Options

defaultMemoize has always been fairly limited. Its signature was (func: Function, equalityCheck?: EqualityFn) => Function, and only ever had a cache size of 1. This has led to many annoyances and workarounds, typically involving calling createSelectorCreator() with a custom memoization function that has a larger cache size or more options for customizing comparisons.

We've updated defaultMemoize to allow cache sizes > 1, as well as customize comparisons of the newly generated result value to improve cache hits.

The signature for defaultMemoize is now:

interface DefaultMemoizeOptions {
  equalityCheck?: EqualityFn
  resultEqualityCheck?: EqualityFn
  maxSize?: number
}

// defaultMemoize now supports a configurable cache size with LRU behavior,
// and optional comparison of the result value with existing values
export function defaultMemoize<F extends (...args: any[]) => any>(
  func: F,
  equalityCheckOrOptions?: EqualityFn | DefaultMemoizeOptions
): F 

In other words, you can still pass equalityCheck as its one additional arg, or you may pass an object containing several possible options.

If the maxSize value is greater than 1, defaultMemoize will now use an LRU cache based on https://github.com/erikras/lru-memoize internally.

If resultEqualityCheck is provided, it will be used to compare the newly-generated value from func against all other values in the cache, in LRU order. If a cached value is found to be equal, that value will be returned. This addresses the common todos.map(todo => todo.id) use case, where a change to any field in any todo object creates a new todos array and thus causes the output to be recalculated, but the generated IDs array is still shallow-equal to the last result. You can now pass an equality function like shallowEqual as the resultEqualityCheck argument, and it will reuse the old IDs array instead.

createSelector Options

Previously, the only way to customize behavior of createSelector was to generate a customized version with createSelectorCreator. By far the most common use case was customizing the equalityCheck option used with defaultMemoize, or using a different memoizer entirely. This usually looked like:

const createShallowEqualSelector = createSelectorCreator(defaultMemoize, shallowEqual)
const createDeepEqualSelector = createSelectorCreator(defaultMemoize, _.isEqual)
const createCustomComparisonSelector = createSelector(_.memoize, hashFn)

createSelectorCreator also accepted additional positional parameters, and forwarded all of them to the provided memoize function, so defaultMemoize ultimately gets called internally as defaultMemoize(actualFunction, shallowEqual).

This added an annoying level of indirection to common customization use cases.

createSelector now accepts an options object as its last argument, after the output selector. Currently, that object only includes one field: memoizeOptions:

interface CreateSelectorOptions<MemoizeOptions extends unknown[]> {
  memoizeOptions: MemoizeOptions[0] | MemoizeOptions
}

Similar to how createSelectorCreator accepts additional "options args" that get forwarded to the memoization function, the memoizeOptions field accepts an array of those "options args" as well. If provided, these override what was given to createSelectorCreator.

That means that you can now customize memoization behavior with direct options to createSelector. And, because defaultMemoize now accepts more options, you can directly customize defaultMemoize's behavior without using createSelectorCreator.

Additionally, because it's very common to only need to pass one options arg to the memoization function, memoizeOptions may also be just that first options arg by itself, without any array.

Example usages of this look like:

  const createSelectorAcceptsArgsAsArray = createSelector(
    (state: StateAB) => state.a,
    (state: StateAB) => state.b,
    (a, b) => a + b,
    {
      // Pass `equalityCheck`, the first options arg of `defaultMemoize`, in an array
      memoizeOptions: [(a, b) => a === b]
    }
  )
  
  const createSelectorFirstArgDirectly = createSelector(
    (state: StateAB) => state.a,
    (state: StateAB) => state.b,
    (a, b) => a + b,
    {
      // Pass `equalityCheck`, the first options arg of `defaultMemoize`, directly
      memoizeOptions: (a, b) => a === b
    }
  )

  const defaultMemoizeAcceptsFirstArgAsObject = createSelector(
    (state: StateAB) => state.a,
    (state: StateAB) => state.b,
    (a, b) => a + b,
    {
      // Pass `options`, the _alternate_ first arg of `defaultMemoize`, directly
      memoizeOptions: {
        equalityCheck: (a, b) => a === b,
        maxSize: 10,
        resultEqualityCheck: shallowEqual
      }
    }
  )

  // Can still create custom selectors by passing args to `createSelectorCreator`
  const customSelectorCreatorMicroMemoize = createSelectorCreator(
    microMemoize,
    {
      maxSize: 42
    }
  )

This should make it much easier to customize behavior.

All of this is fully TypeScript-typed, and the possible values for memoizeOptions should be fully inferred from the provided memoize function.

Additionally, defaultMemoize now supports clearing the cache inside a memoized function (regardless of cache size). The memoized function returned from defaultMemoize will now have a .clearCache() method attached that will clear the cache.

When using createSelector, this can be accessed using selector.memoizedResultFunc.clearCache().

TypeScript Improvements

The Reselect types were written several years ago and originally targeted TS 2.x versions. As a result, the typedefs requires dozens of overloads to handle varying numbers of arguments (see the legacy typedefs file for examples).

We've converted the codebase to be written in TypeScript, and as part of that process we've completely rewritten the TS typedefs to use modern TS syntax like mapped types. This drastically shrinks the size of the typedefs (from 1000 lines to about 115), and also improves the actual type inference overall. Assuming the input selectors are correctly and consistently typed, TS will now fully infer the return values of all input selectors, the arguments to the output selector, and the exact type of the memoized function.

The updated types do require use of TS 4.2+. We've attempted to keep the final public type names and usage the same, but there may also be some types breakage. We'd appreciate feedback on any meaningful breakage issues so we can make further tweaks if needed.

Given the intent of the improvements, that they're all type-only changes, the attempts to retain backwards compatibility, and TS's own versioning scheme, we're considering this to be a minor version change rather than a major.

In pre-release testing, the main issues we saw were:

  • Input selectors that did not declare the type of the state arg. Fix: explicitly add a type to state
  • Selectors that explicitly declare all the generic type arguments, which no longer exist because they will be inferred. Fix: just delete the <A, B, C, D> generics from the createSelector() call.

The legacy types are still included, and should automatically be used if you are using TS 4.1 and earlier. Note that the legacy types do not include the definitions for the new defaultMemoize options - you'll need to be on TS 4.2+ to use those with TS.

Additional Tweaks

We've improved the error messages thrown when invalid selectors are provided.

Generated selectors now include selector.memoizedResultFunc and selector.lastResult for later access if needed.

Changes

The early alphas contained code from several outstanding PRs, pulled together:

Additional work included:

Full Changelog: v4.0.0...v4.1.0