From ea1894ece6bdc59bcbf18eb817037d70b5a175ca Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Tue, 31 Oct 2023 21:36:51 -0500 Subject: [PATCH 01/50] Update JSDocs for `createStructuredSelector` --- src/createStructuredSelector.ts | 49 ++++++++------------------------- 1 file changed, 11 insertions(+), 38 deletions(-) diff --git a/src/createStructuredSelector.ts b/src/createStructuredSelector.ts index 11462b925..42cfe2935 100644 --- a/src/createStructuredSelector.ts +++ b/src/createStructuredSelector.ts @@ -3,9 +3,11 @@ import { createSelector } from './createSelectorCreator' import type { CreateSelectorFunction } from './createSelectorCreator' import type { defaultMemoize } from './defaultMemoize' import type { + InterruptRecursion, ObjectValuesToTuple, OutputSelector, Selector, + Simplify, UnknownMemoizer } from './types' import { assertIsObject } from './utils' @@ -79,48 +81,18 @@ export interface StructuredSelectorCreator { * import { createSelector, createStructuredSelector } from 'reselect' * * interface State { - * todos: { - * id: number - * title: string - * description: string - * completed: boolean - * }[] - * alerts: { - * id: number - * message: string - * type: 'reminder' | 'notification' - * read: boolean - * }[] + * todos: { id: number; completed: boolean }[] + * alerts: { id: number; read: boolean }[] * } * * const state: State = { * todos: [ - * { - * id: 0, - * title: 'Buy groceries', - * description: 'Milk, bread, eggs, and fruits', - * completed: false - * }, - * { - * id: 1, - * title: 'Schedule dentist appointment', - * description: 'Check available slots for next week', - * completed: true - * } + * { id: 0, completed: false }, + * { id: 1, completed: true } * ], * alerts: [ - * { - * id: 0, - * message: 'You have an upcoming meeting at 3 PM.', - * type: 'reminder', - * read: false - * }, - * { - * id: 1, - * message: 'New software update available.', - * type: 'notification', - * read: true - * } + * { id: 0, read: false }, + * { id: 1, read: true } * ] * } * @@ -185,10 +157,11 @@ export interface StructuredSelectorCreator { > ): OutputSelector< ObjectValuesToTuple, - SelectorsMap, + Simplify>, MemoizeFunction, ArgsMemoizeFunction - > + > & + InterruptRecursion // TODO: Do we need this? /** * Second overload From 15feb038415851b7202ca1f66e94d02132bf29b8 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Tue, 31 Oct 2023 21:38:55 -0500 Subject: [PATCH 02/50] Add units tests to showcase `createStructuredSelector`s second function signature is unsafe. --- test/createStructuredSelector.spec.ts | 41 +++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/createStructuredSelector.spec.ts b/test/createStructuredSelector.spec.ts index 058c9aba5..56cc78013 100644 --- a/test/createStructuredSelector.spec.ts +++ b/test/createStructuredSelector.spec.ts @@ -128,4 +128,45 @@ describe('structured selector created with createStructuredSel ) } ) + + localTest( + 'structured selector invalid args can throw runtime errors', + ({ state }) => { + const structuredSelector = createStructuredSelector( + { + allTodos: (state: RootState) => state.todos, + allAlerts: (state: RootState) => state.alerts, + selectedTodo: ( + state: RootState, + id: number, + field: keyof RootState['todos'][number] + ) => state.todos[id][field] + }, + createSelector + ) + const selector = createSelector( + [ + (state: RootState) => state.todos, + (state: RootState) => state.alerts, + ( + state: RootState, + id: number, + field: keyof RootState['todos'][number] + ) => state.todos[id][field] + ], + (allTodos, allAlerts, selectedTodo) => { + return { + allTodos, + allAlerts, + selectedTodo + } + } + ) + // These two cases are the same. + // @ts-expect-error + expect(() => structuredSelector(state)).toThrowError(TypeError) + // @ts-expect-error + expect(() => selector(state)).toThrowError(TypeError) + } + ) }) From e4fe262b9fe2d8480d5ecda2dd326a0e3c9d72fb Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Tue, 31 Oct 2023 21:43:27 -0500 Subject: [PATCH 03/50] Add more type tests - Also add some of the memoization related types to exports. --- src/defaultMemoize.ts | 3 + src/index.ts | 6 +- src/types.ts | 114 ++++++++++---------- test/testUtils.ts | 15 ++- typescript_test/argsMemoize.typetest.ts | 136 ++++++++++++++++++++++++ 5 files changed, 214 insertions(+), 60 deletions(-) diff --git a/src/defaultMemoize.ts b/src/defaultMemoize.ts index a262e35c3..a5c873356 100644 --- a/src/defaultMemoize.ts +++ b/src/defaultMemoize.ts @@ -88,6 +88,9 @@ function createLruCache(maxSize: number, equals: EqualityFn): Cache { } /** + * Runs a simple reference equality check. + * What {@linkcode defaultMemoize defaultMemoize} uses by default. + * * @public */ export const defaultEqualityCheck: EqualityFn = (a, b): boolean => { diff --git a/src/index.ts b/src/index.ts index c6db78efe..9ad07492e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,15 +16,19 @@ export type { Combiner, CreateSelectorOptions, EqualityFn, + ExtractMemoizerFields, GetParamsFromSelectors, GetStateFromSelectors, + MemoizeOptionsFromParameters, OutputParametricSelector, OutputSelector, OutputSelectorFields, + OverrideMemoizeOptions, ParametricSelector, Selector, SelectorArray, SelectorResultArray, - StabilityCheckFrequency + StabilityCheckFrequency, + UnknownMemoizer } from './types' export { weakMapMemoize } from './weakMapMemoize' diff --git a/src/types.ts b/src/types.ts index c79719a37..c50128c77 100644 --- a/src/types.ts +++ b/src/types.ts @@ -311,50 +311,37 @@ export type GetParamsFromSelectors = ArrayTail< MergeParameters > -/* - * ----------------------------------------------------------------------------- - * ----------------------------------------------------------------------------- - * - * Reselect Internal Utility Types - * - * ----------------------------------------------------------------------------- - * ----------------------------------------------------------------------------- - */ - -/** - * Any function with any arguments. - * - * @internal - */ -export type AnyFunction = (...args: any[]) => any - -/** - * Any function with unknown arguments. - * - * @internal - */ -export type UnknownFunction = (...args: unknown[]) => unknown - /** * Any Memoizer function. A memoizer is a function that accepts another function and returns it. * * @template FunctionType - The type of the function that is memoized. * - * @internal + * @public */ export type UnknownMemoizer< FunctionType extends UnknownFunction = UnknownFunction > = (func: FunctionType, ...options: any[]) => FunctionType /** - * When a generic type parameter is using its default value of `never`, fallback to a different type. + * Extracts the options type for a memoization function based on its parameters. + * The first parameter of the function is expected to be the function to be memoized, + * followed by options for the memoization process. * - * @template T - Type to be checked. - * @template FallbackTo - Type to fallback to if `T` resolves to `never`. + * @template MemoizeFunction - The type of the memoize function to be checked. * - * @internal + * @public */ -export type FallbackIfNever = IfNever +export type MemoizeOptionsFromParameters< + MemoizeFunction extends UnknownMemoizer +> = + | ( + | Simplify[0]>> + | FunctionType[0]> + ) + | ( + | Simplify[number]>> + | FunctionType[number]> + )[] /** * Derive the type of memoize options object based on whether the memoize function itself was overridden. @@ -364,7 +351,7 @@ export type FallbackIfNever = IfNever * @template MemoizeFunction - The type of the `memoize` or `argsMemoize` function initially passed into `createSelectorCreator`. * @template OverrideMemoizeFunction - The type of the optional `memoize` or `argsMemoize` function passed directly into `createSelector` which then overrides the original `memoize` or `argsMemoize` function passed into `createSelectorCreator`. * - * @internal + * @public */ export type OverrideMemoizeOptions< MemoizeFunction extends UnknownMemoizer, @@ -376,54 +363,67 @@ export type OverrideMemoizeOptions< > /** - * Extracts the non-function part of a type. + * Extracts the additional fields that a memoize function attaches to + * the function it memoizes (e.g., `clearCache`). * - * @template T - The input type to be refined by excluding function types and index signatures. + * @template MemoizeFunction - The type of the memoize function to be checked. + * + * @public + */ +export type ExtractMemoizerFields = + Simplify>> + +/* + * ----------------------------------------------------------------------------- + * ----------------------------------------------------------------------------- + * + * Reselect Internal Utility Types + * + * ----------------------------------------------------------------------------- + * ----------------------------------------------------------------------------- + */ + +/** + * Any function with any arguments. * * @internal */ -export type NonFunctionType = OmitIndexSignature> +export type AnyFunction = (...args: any[]) => any /** - * Extracts the function part of a type. + * Any function with unknown arguments. * - * @template T - The input type to be refined by extracting function types. + * @internal + */ +export type UnknownFunction = (...args: unknown[]) => unknown + +/** + * When a generic type parameter is using its default value of `never`, fallback to a different type. + * + * @template T - Type to be checked. + * @template FallbackTo - Type to fallback to if `T` resolves to `never`. * * @internal */ -export type FunctionType = Extract +export type FallbackIfNever = IfNever /** - * Extracts the options type for a memoization function based on its parameters. - * The first parameter of the function is expected to be the function to be memoized, - * followed by options for the memoization process. + * Extracts the non-function part of a type. * - * @template MemoizeFunction - The type of the memoize function to be checked. + * @template T - The input type to be refined by excluding function types and index signatures. * * @internal */ -export type MemoizeOptionsFromParameters< - MemoizeFunction extends UnknownMemoizer -> = - | ( - | Simplify[0]>> - | FunctionType[0]> - ) - | ( - | Simplify[number]>> - | FunctionType[number]> - )[] +export type NonFunctionType = OmitIndexSignature> /** - * Extracts the additional fields that a memoize function attaches to - * the function it memoizes (e.g., `clearCache`). + * Extracts the function part of a type. * - * @template MemoizeFunction - The type of the memoize function to be checked. + * @template T - The input type to be refined by extracting function types. * * @internal */ -export type ExtractMemoizerFields = - Simplify>> +export type FunctionType = Extract /** * Extracts the return type from all functions as a tuple. diff --git a/test/testUtils.ts b/test/testUtils.ts index 608a2d988..c3f6beb78 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -1,5 +1,6 @@ import type { PayloadAction } from '@reduxjs/toolkit' import { combineReducers, configureStore, createSlice } from '@reduxjs/toolkit' +import type { AnyFunction, Simplify } from '../src/types' interface Todo { id: number @@ -172,9 +173,9 @@ const rootReducer = combineReducers({ export const setupStore = () => configureStore({ reducer: rootReducer }) -export type AppStore = ReturnType +export type AppStore = Simplify> -export type RootState = ReturnType +export type RootState = Simplify> export interface LocalTestContext { store: AppStore @@ -194,3 +195,13 @@ export const { // Since Node 16 does not support `structuredClone` export const deepClone = (object: T): T => JSON.parse(JSON.stringify(object)) + +export const setFunctionName = (func: AnyFunction, name: string) => { + Object.defineProperty(func, 'name', { value: name }) +} + +export const setFunctionNames = (funcObject: Record) => { + Object.entries(funcObject).forEach(([key, value]) => + setFunctionName(value, key) + ) +} diff --git a/typescript_test/argsMemoize.typetest.ts b/typescript_test/argsMemoize.typetest.ts index acb9b1df0..649debf4e 100644 --- a/typescript_test/argsMemoize.typetest.ts +++ b/typescript_test/argsMemoize.typetest.ts @@ -776,6 +776,24 @@ function deepNesting() { selector20.dependencies[0].dependencies[0].dependencies[0].dependencies[0] .dependencies[0].dependencies[0].dependencies[0].dependencies[0] .dependencies[0].dependencies[0].memoizedResultFunc.cache + const selector21 = createSelector(selector20, s => s) + const selector22 = createSelector(selector21, s => s) + const selector23 = createSelector(selector22, s => s) + const selector24 = createSelector(selector23, s => s) + const selector25 = createSelector(selector24, s => s) + const selector26 = createSelector(selector25, s => s) + const selector27 = createSelector(selector26, s => s) + const selector28 = createSelector(selector27, s => s) + const selector29 = createSelector(selector28, s => s) + const selector30 = createSelector(selector29, s => s) + selector30.dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].memoizedResultFunc.clearCache } function deepNesting1() { @@ -908,3 +926,121 @@ function deepNesting2() { memoize: defaultMemoize }) } + +function parameterLimit() { + const selector = createSelector( + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testBoolean: boolean }) => state.testBoolean, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testStringArray: string[] }) => state.testStringArray, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testBoolean: boolean }) => state.testBoolean, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testStringArray: string[] }) => state.testStringArray, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testBoolean: boolean }) => state.testBoolean, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testStringArray: string[] }) => state.testStringArray, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testBoolean: boolean }) => state.testBoolean, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testStringArray: string[] }) => state.testStringArray, + ( + foo1: string, + foo2: number, + foo3: boolean, + foo4: string, + foo5: string, + foo6: string, + foo7: string, + foo8: number, + foo9: string[], + foo10: string, + foo11: number, + foo12: boolean, + foo13: string, + foo14: string, + foo15: string, + foo16: string, + foo17: number, + foo18: string[], + foo19: string, + foo20: number, + foo21: boolean, + foo22: string, + foo23: string, + foo24: string, + foo25: string, + foo26: number, + foo27: string[], + foo28: string, + foo29: number, + foo30: boolean, + foo31: string, + foo32: string, + foo33: string, + foo34: string, + foo35: number, + foo36: string[] + ) => { + return { + foo1, + foo2, + foo3, + foo4, + foo5, + foo6, + foo7, + foo8, + foo9, + foo10, + foo11, + foo12, + foo13, + foo14, + foo15, + foo16, + foo17, + foo18, + foo19, + foo20, + foo21, + foo22, + foo23, + foo24, + foo25, + foo26, + foo27, + foo28, + foo29, + foo30, + foo31, + foo32, + foo33, + foo34, + foo35, + foo36 + } + } + ) +} From f8cdab2a307f359bdde9ef62bc1e437bbf628a39 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Fri, 10 Nov 2023 22:52:27 -0600 Subject: [PATCH 04/50] Fix `README` for v5. - Add documentation regarding new features. - Redo `CodeSandbox` examples to align better with documentation. Solves #598. - Fix GitHub workflow badge URL #628. - Add instructions for `bun` and `pnpm`. Solves #621. - Fix minor type issues regarding `createStructuredSelector`. Solves #499. - `argsMemoize` and `argsMemoizeOptions` solve #359. - Remove `Redux` legacy patterns including `connect` and `switch` statements from docs. Solves #515. - Replace legacy code patterns with modern syntax like `useSelector` and functional components. - Implementation of `argsMemoize` solves #376. - Document order of execution in `Reselect`. Solves #455. - Add benchmarks to confirm the info inside the documentation. - Add more type tests with `vitest`. - Fix more hover preview issues with types. --- README.md | 2189 +++++++++++++---- .../normal-memoization-function.drawio | 31 + .../diagrams/reselect-memoization.drawio | 40 + docs/assets/normal-memoization-function.png | Bin 0 -> 17789 bytes docs/assets/reselect-memoization.png | Bin 0 -> 35089 bytes package.json | 3 +- src/autotrackMemoize/autotrackMemoize.ts | 14 +- src/createCurriedSelectorCreator.ts | 132 + src/createSelectorCreator.ts | 97 +- src/createStructuredSelector.ts | 16 +- src/defaultMemoize.ts | 9 +- src/index.ts | 6 + src/types.ts | 158 +- src/utils.ts | 2 +- src/weakMapMemoize.ts | 58 +- test/autotrackMemoize.spec.ts | 8 +- .../benchmarks/createCurriedSelector.bench.ts | 53 + test/benchmarks/orderOfExecution.bench.ts | 117 + test/benchmarks/reselect.bench.ts | 292 +++ test/benchmarks/weakMapMemoize.bench.ts | 442 ++++ test/reselect.bench.ts | 217 -- test/reselect.spec.ts | 215 +- test/selectorUtils.spec.ts | 1 + test/testUtils.ts | 249 +- test/tsconfig.json | 12 +- test/weakmapMemoize.spec.ts | 2 +- tsconfig.json | 5 +- type-tests/argsMemoize.test-d.ts | 894 +++++++ type-tests/createCurriedSrlector.test-d.ts | 106 + type-tests/deepNesting.test-d.ts | 320 +++ type-tests/tsconfig.json | 14 + typescript_test/argsMemoize.typetest.ts | 290 ++- typescript_test/test.ts | 24 +- typescript_test/typesTestUtils.ts | 23 + vitest.config.ts | 1 + 35 files changed, 5027 insertions(+), 1013 deletions(-) create mode 100644 docs/assets/diagrams/normal-memoization-function.drawio create mode 100644 docs/assets/diagrams/reselect-memoization.drawio create mode 100644 docs/assets/normal-memoization-function.png create mode 100644 docs/assets/reselect-memoization.png create mode 100644 src/createCurriedSelectorCreator.ts create mode 100644 test/benchmarks/createCurriedSelector.bench.ts create mode 100644 test/benchmarks/orderOfExecution.bench.ts create mode 100644 test/benchmarks/reselect.bench.ts create mode 100644 test/benchmarks/weakMapMemoize.bench.ts delete mode 100644 test/reselect.bench.ts create mode 100644 type-tests/argsMemoize.test-d.ts create mode 100644 type-tests/createCurriedSrlector.test-d.ts create mode 100644 type-tests/deepNesting.test-d.ts create mode 100644 type-tests/tsconfig.json create mode 100644 typescript_test/typesTestUtils.ts diff --git a/README.md b/README.md index 0c4e6518b..919167177 100644 --- a/README.md +++ b/README.md @@ -1,686 +1,1830 @@ # Reselect +![TypeScript][typescript-badge][![npm package][npm-badge]][npm][![Coveralls][coveralls-badge]][coveralls][![GitHub Workflow Status][build-badge]][build] + A library for creating memoized "selector" functions. Commonly used with Redux, but usable with any plain JS immutable data as well. -- Selectors can compute derived data, allowing Redux to store the minimal possible state. -- Selectors are efficient. A selector is not recomputed unless one of its arguments changes. -- Selectors are composable. They can be used as input to other selectors. +- Selectors can compute derived data, allowing Redux to store the minimal possible state. +- Selectors are efficient. A selector is not recomputed unless one of its arguments changes. +- Selectors are composable. They can be used as input to other selectors. + +The **Redux docs usage page on [Deriving Data with Selectors](https://redux.js.org/usage/deriving-data-selectors)** covers the purpose and motivation for selectors, why memoized selectors are useful, typical `Reselect` usage patterns, and using selectors with React-Redux. + +## Installation + +### Redux Toolkit + +While `Reselect` is not exclusive to Redux, it is already included by default in [the official Redux Toolkit package](https://redux-toolkit.js.org) - no further installation needed. + +```ts +import { createSelector } from '@reduxjs/toolkit' +``` + +### Standalone + +For standalone usage, install the `Reselect` package: + +#### Using `npm` + +```bash +npm install reselect +``` + +#### Using `yarn` + +```bash +yarn add reselect +``` + +#### Using `bun` + +```bash +bun add reselect +``` + +#### Using `pnpm` + +```bash +pnpm add reselect +``` + +--- + +## Basic Usage + +`Reselect` exports a `createSelector` API, which generates memoized selector functions. `createSelector` accepts one or more `input selectors`, which extract values from arguments, and a `combiner` function that receives the extracted values and should return a derived value. If the generated `output selector` is called multiple times, the output will only be recalculated when the extracted values have changed. + +You can play around with the following **example** in [this CodeSandbox](https://codesandbox.io/s/reselect-example-g3k9gf?file=/src/index.js): + +```ts +import { createSelector } from 'reselect' + +interface RootState { + todos: { id: number; completed: boolean }[] + alerts: { id: number; read: boolean }[] +} + +const state: RootState = { + todos: [ + { id: 0, completed: false }, + { id: 1, completed: true } + ], + alerts: [ + { id: 0, read: false }, + { id: 1, read: true } + ] +} + +const selectCompletedTodos = (state: RootState) => { + console.log('selector ran') + return state.todos.filter(todo => todo.completed === true) +} + +selectCompletedTodos(state) // selector ran +selectCompletedTodos(state) // selector ran +selectCompletedTodos(state) // selector ran + +const memoizedSelectCompletedTodos = createSelector( + [(state: RootState) => state.todos], + todos => { + console.log('memoized selector ran') + return todos.filter(todo => todo.completed === true) + } +) + +memoizedSelectCompletedTodos(state) // memoized selector ran +memoizedSelectCompletedTodos(state) +memoizedSelectCompletedTodos(state) + +console.log(selectCompletedTodos(state) === selectCompletedTodos(state)) //=> false + +console.log( + memoizedSelectCompletedTodos(state) === memoizedSelectCompletedTodos(state) +) //=> true +``` + +As you can see from the example above, `memoizedSelectCompletedTodos` does not run the second or third time, but we still get the same return value as last time. + +Another difference is that with `memoizedSelectCompletedTodos` the referential integrity of the return value is also maintained through multiple calls of the selector, but the same cannot be said about the first example. + +--- + +## Table of Contents + +- [Installation](#installation) + - [Redux Toolkit](#redux-toolkit) + - [Standalone](#standalone) +- [Basic Usage](#basic-usage) +- [API](#api) + - [**`createSelector`**](#createselector) + - [**`defaultMemoize`**](#defaultmemoize) + - [**`weakMapMemoize`**](#weakmapmemoize) + - [**`autotrackMemoize`**](#autotrackmemoize) + - [**`createSelectorCreator`**](#createselectorcreator) + - [**`createStructuredSelector`**](#createstructuredselector) + - [**`createCurriedSelector`**](#createcurriedselector) +- [Debugging Tools](#debuggingtools) +- [FAQ](#faq) + - [Why isn’t my selector recomputing when the input state changes?](#why-isnt-my-selector-recomputing-when-the-input-state-changes) + - [Why is my selector recomputing when the input state stays the same?](#why-is-my-selector-recomputing-when-the-input-state-stays-the-same) + - [Can I use Reselect without Redux?](#can-i-use-reselect-without-redux) + - [How do I create a selector that takes an argument?](#how-do-i-create-a-selector-that-takes-an-argument) + - [The default memoization function is no good, can I use a different one?](#the-default-memoization-function-is-no-good-can-i-use-a-different-one) + - [How do I test a selector?](#how-do-i-test-a-selector) + - [Can I share a selector across multiple component instances?](#can-i-share-a-selector-across-multiple-component-instances) + - [Are there TypeScript Typings?](#are-there-typescript-typings) + - [I am seeing a TypeScript error: `Type instantiation is excessively deep and possibly infinite`](#i-am-seeing-a-typescript-error-type-instantiation-is-excessively-deep-and-possibly-infinite) + - [How can I make a curried selector?](#how-can-i-make-a-curried-selector) +- [Related Projects](#related-projects) +- [License](#license) +- [Prior Art and Inspiration](#prior-art-and-inspiration) + +--- + +
+ + + +## Terminology + + + +- [**`Selector Function`**](#selector-function): Any function that accepts the Redux store state (or part of the state) as an argument, and returns data that is based on that state. +- [**`Input Selectors`**](#input-selectors): Standard selector functions that are used to create the result function. +- [**`Output Selector`**](#output-selector): The actual selectors generated by calling `createSelector`. +- [**`Result Function`**](#result-function): The function that comes after the input selectors. It takes input selectors' return values as arguments and returns a result. Otherwise known as the `combiner`. +- [**`Combiner`**](#combiner): A function that takes input selectors' return values as arguments and returns a result. This term is somewhat interchangeably used with `resultFunc`. But `combiner` is more of a general term and `resultFunc` is more specific to `Reselect`. So the `resultFunc` is a `combiner` but a `combiner` is not necessarily the same as `resultFunc`. +- [**`Dependencies`**](#dependencies): Same as input selectors. They are what the output selector "depends" on. + +The below example serves as a visual aid. + +```ts +const outputSelector = createSelector( + [inputSelector1, inputSelector2, inputSelector3], // synonymous with `dependencies`. + combiner // synonymous with `Result Function` or `resultFunc`. +) +``` + +
+ + + +--- + +## How Does `Reselect` Work? + + + +### Cascading Memoization + + + +The way `Reselect` works can be broken down into multiple parts: + +1. **Initial Run**: On the first call, `Reselect` runs all the `input selectors`, gathers their results, and passes them to the `result function`. + +2. **Subsequent Runs**: For subsequent calls, `Reselect` performs two levels of checks: + + - **First Level**: It compares the current arguments with the previous ones. + + - If they're the same, it returns the cached result without running the `input selectors` or the `result function`. + + - If they differ, it proceeds to the second level. + + - **Second Level**: It runs the `input selectors` and compares their current results with the previous ones. + > [!NOTE] + > If any one of the `input selectors` return a different result, all `input selectors` will recalculate. + - If the results are the same, it returns the cached result without running the `result function`. + - If the results differ, it runs the `result function`. + +This behavior is what we call **_Cascading Double-Layer Memoization_**. + + + +### `Reselect` Vs Standard Memoization + +##### Standard Memoization + +![normal-memoization-function](docs/assets//normal-memoization-function.png) + +_Standard memoization only compares arguments. If they're the same, it returns the cached result._ + +##### Memoization with `Reselect` + +![reselect-memoization](docs/assets//reselect-memoization.png) + +_`Reselect` adds a second layer of checks with the `input selectors`. This is crucial in `Redux` applications where state references change frequently._ + +A normal memoization function will compare the arguments, and if they are the same as last time, it will skip running the function and return the cached result. `Reselect`'s behavior differs from a simple memoization function in that, it adds a second layer of checks through the `input selectors`. So what can sometimes happen is that the arguments that get passed to the `input selectors` change, but the result of the `input selectors` still remain the same, and that means we can still skip running the `result function`. + +This is especially important since in a `Redux` application, your `state` is going to change its reference any time you make an update through dispatched actions. + +> [!NOTE] +> The `input selectors` take the same arguments as the `output selector`. + +#### Why `Reselect` Is Often Used With `Redux` + +Imagine you have a selector like this: + +```ts +const selectCompletedTodos = (state: RootState) => + state.todos.filter(todo => todo.completed === true) +``` + +So you decide to memoize it: + +```ts +const selectCompletedTodos = someMemoizeFunction((state: RootState) => + state.todos.filter(todo => todo.completed === true) +) +``` + +Then you update `state.alerts`: + +```ts +store.dispatch(toggleRead(0)) +``` + +Now when you call `selectCompletedTodos`, it re-runs, because we have effectively broken memoization. + +```ts +selectCompletedTodos(store.getState()) +selectCompletedTodos(store.getState()) // Will not run, and the cached result will be returned. +store.dispatch(toggleRead(0)) +selectCompletedTodos(store.getState()) // It recalculates. +``` + +But why? `selectCompletedTodos` only needs to access `state.todos`, and has nothing to do with `state.alerts`, so why have we broken memoization? Well that's because in `Redux` anytime you make a change to the root `state`, it gets shallowly updated, which means its reference changes, therefore a normal memoization function will always fail the comparison check on the arguments. + +But with `Reselect`, we can do something like this: + +```ts +const selectCompletedTodos = createSelector( + [(state: RootState) => state.todos], + todos => todos.filter(todo => todo.completed === true) +) +``` + +And now we have achieved memoization: + +```ts +selectCompletedTodos(store.getState()) +selectCompletedTodos(store.getState()) // Will not run, and the cached result will be returned. +store.dispatch(toggleRead(0)) +selectCompletedTodos(store.getState()) // The `input selectors` will run, But the `result function` is skipped and the cached result will be returned. +``` + +Even though our `state` changes, and we fail the comparison check on the arguments, the `result function` will not run because the current `state.todos` is still the same as the previous `state.todos`. So we have at least partial memoization, because the memoization "cascades" and goes from the arguments to the results of the `input selectors`. So basically there are "2 layers of checks" and in this situation, while the first one fails, the second one succeeds. And that is why this type of `Cascading Double-Layer Memoization` makes `Reselect` a good candidate to be used with `Redux`. + + + +--- + +## API + + + +### createSelector(...inputSelectors | [inputSelectors], resultFunc, createSelectorOptions?) + +
+ + + +#### Description + + + +Accepts one or more ["input selectors"](#input-selectors) (either as separate arguments or a single array), +a single ["result function"](#result-function) / ["combiner"](#combiner), and an optional options object, and +generates a memoized selector function. + +
+ +
+ + + +#### Parameters + + + +| Name | Description | +| :----------------------- | :---------------------------------------------------------------------------------------------------------------------------- | +| `inputSelectors` | An array of [`input selectors`](#input-selectors), can also be passed as separate arguments. | +| `combiner` | A [`combiner`](#combiner) function that takes the results of the [`input selectors`](#input-selectors) as separate arguments. | +| `createSelectorOptions?` | An optional options object that allows for further customization per selector. | + +
+ +
+ + + +#### Returns + + + +A memoized [`output selector`](#output-selector). + +
+ +
+ + + +#### Type parameters + + + +| Name | Description | +| :---------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `InputSelectors` | The type of the [`input selectors`](#input-selectors) array. | +| `Result` | The return type of the [`combiner`](#combiner) as well as the [`output selector`](#output-selector). | +| `OverrideMemoizeFunction` | The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`. | +| `OverrideArgsMemoizeFunction` | The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`. | + +
+ + + +--- + +### Memoization Functions + + + +#### defaultMemoize(func, equalityCheckOrOptions = defaultEqualityCheck) + +
+ + + +##### Description + + + +The standard memoize function used by `createSelector`. + +It has a default cache size of 1. This means it always recalculates when the value of an argument changes. However, this can be customized as needed with a specific max cache size (**`Since`** 4.1.0). + +It determines if an argument has changed by calling the `equalityCheck` function. As `defaultMemoize` is designed to be used with immutable data, the default `equalityCheck` function checks for changes using [`reference equality`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality): + +```ts +const defaultEqualityCheck = (previousValue: any, currentValue: any) => { + return previousValue === currentValue +} +``` + +
+ +
+ + + +##### Parameters + + + +| Name | Description | +| :----------------------- | :---------------------------------------------------------- | +| `func` | The function to be memoized. | +| `equalityCheckOrOptions` | Either an `equality check` function or an `options` object. | + +**`Since`** 4.1.0, `defaultMemoize` also accepts an options object as its first argument instead of an `equalityCheck` function. The `options` object may contain: + +```ts +type EqualityFn = (a: any, b: any) => boolean + +interface DefaultMemoizeOptions { + equalityCheck?: EqualityFn + resultEqualityCheck?: EqualityFn + maxSize?: number +} +``` + +| Name | Description | +| :-------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `equalityCheck` | Used to compare the individual arguments of the provided calculation function.
**`Default`** = `defaultEqualityCheck` | +| `resultEqualityCheck` | If provided, used to compare a newly generated output value against previous values in the cache. If a match is found, the old value is returned. This addresses the common todos.map(todo => todo.id) use case, where an update to another field in the original data causes a recalculation due to changed references, but the output is still effectively the same. | +| `maxSize` | The cache size for the selector. If greater than 1, the selector will use an LRU cache internally.
**`Default`** = 1 | + +> [!WARNING] +> If `resultEqualityCheck` is used inside `argsMemoizeOptions` it has no effect. + +
+ +
+ + + +##### Returns + + + +A memoized function with a `.clearCache()` method attached. + +
+ +
+ + + +##### Type parameters + + + +| Name | Description | +| :----- | :----------------------------------------- | +| `Func` | The type of the function that is memoized. | + +
+ +
+ + + +##### **`Examples`** + + + +###### Using `createSelector` + +```ts +import { shallowEqual } from 'react-redux' +import { createSelector } from 'reselect' + +const selectTodoIds = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(todo => todo.id), + { + memoizeOptions: { + equalityCheck: shallowEqual, + resultEqualityCheck: shallowEqual, + maxSize: 10 + }, + argsMemoizeOptions: { + equalityCheck: shallowEqual, + resultEqualityCheck: shallowEqual, + maxSize: 10 + } + } +) +``` + +###### Using `createSelectorCreator` + +```ts +import { shallowEqual } from 'react-redux' +import { createSelectorCreator, defaultMemoize } from 'reselect' + +const createSelectorShallowEqual = createSelectorCreator({ + memoize: defaultMemoize, + memoizeOptions: { + equalityCheck: shallowEqual, + resultEqualityCheck: shallowEqual, + maxSize: 10 + }, + argsMemoize: defaultMemoize, + argsMemoizeOptions: { + equalityCheck: shallowEqual, + resultEqualityCheck: shallowEqual, + maxSize: 10 + } +}) + +const selectTodoIds = createSelectorShallowEqual( + [(state: RootState) => state.todos], + todos => todos.map(todo => todo.id) +) +``` + +
+ + + +--- + + + +#### weakMapMemoize(func) - (**`Since`** 5.0.0) + +
+ + + +##### Description + + + +`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 selectors). **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. + +**Design Tradeoffs for `weakMapMemoize`:** + +- Pros: + - 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 `WeakMap`s. +- Cons: + - There's currently no way to alter the argument comparisons. They're based on strict reference equality. + +**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: + +```ts +useSelector(state => selectSomeData(state, id)) +``` + +
+ +
+ + + +##### Parameters + + + +| Name | Description | +| :----- | :--------------------------- | +| `func` | The function to be memoized. | + +
+ +
+ + + +##### Returns + + + +A memoized function with a `.clearCache()` method attached. + +
+ +
+ + + +##### Type parameters + + + +| Name | Description | +| :----- | :----------------------------------------- | +| `Func` | The type of the function that is memoized. | + +
+ +
+ + + +##### **`Examples`** + + + +Prior to `weakMapMemoize`, you had this problem: + +```ts +const parametricSelector = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.filter(todo => todo.id === id) +) + +parametricSelector(state, 0) // Selector runs +parametricSelector(state, 0) +parametricSelector(state, 1) // Selector runs +parametricSelector(state, 0) // Selector runs again! +``` + +Before you could solve this in a number of different ways: + +1. Set the `maxSize` with `defaultMemoize`: + +```ts +const parametricSelector = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.filter(todo => todo.id === id), + { + memoizeOptions: { + maxSize: 10 + } + } +) +``` + +But this required having to know the cache size ahead of time. + +2. Create unique selector instances using `useMemo`. + +```tsx +const parametricSelector = (id: number) => + createSelector([(state: RootState) => state.todos], todos => + todos.filter(todo => todo.id === id) + ) + +const MyComponent: FC = ({ id }) => { + const selectTodosById = useMemo(() => parametricSelector(id), [id]) + + const todosById = useSelector(selectTodosById) + + return ( +
+ {todosById.map(todo => ( +
{todo.title}
+ ))} +
+ ) +} +``` + +3. Using `useCallback`. + +```tsx +const parametricSelector = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.filter(todo => todo.id === id) +) + +const MyComponent: FC = ({ id }) => { + const selectTodosById = useCallback(parametricSelector, []) + + const todosById = useSelector(state => selectTodosById(state, id)) + + return ( +
+ {todosById.map(todo => ( +
{todo.title}
+ ))} +
+ ) +} +``` + +4. Use `re-reselect`: + +```ts +import { createCachedSelector } from 're-reselect' + +const parametricSelector = createCachedSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.filter(todo => todo.id === id) +)((state: RootState, id: number) => id) +``` + +Starting in 5.0.0, you can eliminate this problem using `weakMapMemoize`. + +###### Using `createSelector` + +```ts +const parametricSelector = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.filter(todo => todo.id === id), + { + memoize: weakMapMemoize, + argsMemoize: weakMapMemoize + } +) + +parametricSelector(state, 0) // Selector runs +parametricSelector(state, 0) +parametricSelector(state, 1) // Selector runs +parametricSelector(state, 0) +``` + +###### Using `createSelectorCreator` + +```ts +import { createSelectorCreator, weakMapMemoize } from 'reselect' + +const createSelectorWeakMap = createSelectorCreator({ + memoize: weakMapMemoize, + argsMemoize: weakMapMemoize +}) + +const parametricSelector = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.filter(todo => todo.id === id) +) + +parametricSelector(state, 0) // Selector runs +parametricSelector(state, 0) +parametricSelector(state, 1) // Selector runs +parametricSelector(state, 0) +``` + +This solves the problem of having to know and set the cache size prior to creating a memoized selector. + +
+ + + +--- + + + +#### autotrackMemoize(func) - (**`Since`** 5.0.0) + +
+ + + +##### Description + + + +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. + +
+ +
+ + + +##### Parameters + + + +| Name | Description | +| :----- | :--------------------------- | +| `func` | The function to be memoized. | + +
+ +
+ + + +##### Returns + + + +A memoized function with a `.clearCache()` method attached. + +
+ +
+ + + +##### Type parameters + + + +| Name | Description | +| :----- | :----------------------------------------- | +| `Func` | The type of the function that is memoized. | + +
+ +
+ + + +##### **`Examples`** + + + +###### Using `createSelector` + +```ts +import { + unstable_autotrackMemoize as autotrackMemoize, + createSelector +} from 'reselect' + +const selectTodoIds = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(todo => todo.id), + { memoize: autotrackMemoize } +) +``` + +###### Using `createSelectorCreator` + +```ts +import { + unstable_autotrackMemoize as autotrackMemoize, + createSelectorCreator +} from 'reselect' + +const createSelectorAutotrack = createSelectorCreator({ + memoize: autotrackMemoize +}) + +const selectTodoIds = createSelectorAutotrack( + [(state: RootState) => state.todos], + todos => todos.map(todo => todo.id) +) +``` + +**Design Tradeoffs for autotrackMemoize:** + +- Pros: + - It is likely to avoid excess calculations and recalculate fewer times than defaultMemoize will, which may also result in fewer component re-renders. +- Cons: + + - 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: + + ```ts + 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. + +**Use Cases for `autotrackMemoize`:** + +- It 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. + +Using `createSelector` + +```ts +import { + unstable_autotrackMemoize as autotrackMemoize, + createSelector +} from 'reselect' + +const selectTodoIds = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(todo => todo.id), + { memoize: autotrackMemoize } +) +``` + +Using `createSelectorCreator` + +```ts +import { + unstable_autotrackMemoize as autotrackMemoize, + createSelectorCreator +} from 'reselect' + +const createSelectorAutotrack = createSelectorCreator({ + memoize: autotrackMemoize +}) + +const selectTodoIds = createSelectorAutotrack( + [(state: RootState) => state.todos], + todos => todos.map(todo => todo.id) +) +``` + +
+ + + +--- + + + +### createSelectorCreator(memoize | options, ...memoizeOptions) + +
+ + + +#### Description + + + +Accepts either a `memoize` function and `...memoizeOptions` rest parameter, or **`Since`** 5.0.0 an `options` object containing a `memoize` function and creates a custom selector creator function. + +
+ +
+ + + +#### Parameters (**`Since`** 5.0.0) + + + +| Name | Description | +| :----------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `options` | An options object containing the `memoize` function responsible for memoizing the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). It also provides additional options for customizing memoization. While the `memoize` property is mandatory, the rest are optional. | +| `options.argsMemoize?` | The optional memoize function that is used to memoize the arguments passed into the `output selector` generated by `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`).
**`Default`** `defaultMemoize` | +| `options.argsMemoizeOptions?` | Optional configuration options for the `argsMemoize` function. These options are passed to the `argsMemoize` function as the second argument.
**`Since`** 5.0.0 | +| `options.inputStabilityCheck?` | Overrides the global input stability check for the selector. Possible values are:
`once` - Run only the first time the selector is called.
`always` - Run every time the selector is called.
`never` - Never run the input stability check.
**`Default`** = `'once'`
**`Since`** 5.0.0 | +| `options.memoize` | The memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). **`Since`** 5.0.0 | +| `options.memoizeOptions?` | Optional configuration options for the `memoize` function. These options are passed to the `memoize` function as the second argument.
**`Since`** 5.0.0 | + +
+ +
+ + + +#### Parameters + + + +| Name | Description | +| :-------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| `memoize` | The `memoize` function responsible for memoizing the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). | +| `...memoizeOptionsFromArgs` | Optional configuration options for the memoization function. These options are then passed to the memoize function as the second argument onwards. | + +
+ +
+ + + +#### Returns + + + +A customized `createSelector` function. + +
+ +
+ + + +#### Type parameters + + + +| Name | Description | +| :-------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `MemoizeFunction` | The type of the memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). | +| `ArgsMemoizeFunction` | The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used. | + +
+ +
+ + + +#### **`Examples`** + + + +##### Using `options` (**`Since`** 5.0.0) + +```ts +const customCreateSelector = createSelectorCreator({ + memoize: customMemoize, // Function to be used to memoize `resultFunc` + memoizeOptions: [memoizeOption1, memoizeOption2], // Options passed to `customMemoize` as the second argument onwards + argsMemoize: customArgsMemoize, // Function to be used to memoize the selector's arguments + argsMemoizeOptions: [argsMemoizeOption1, argsMemoizeOption2] // Options passed to `customArgsMemoize` as the second argument onwards +}) + +const customSelector = customCreateSelector( + [inputSelector1, inputSelector2], + resultFunc // `resultFunc` will be passed as the first argument to `customMemoize` +) + +customSelector( + ...selectorArgs // Will be memoized by `customArgsMemoize` +) +``` + + + +--- + +##### Using `memoize` and `...memoizeOptions` + +`createSelectorCreator` can be used to make a customized version of `createSelector`. + +The `memoize` argument is a memoization function to replace `defaultMemoize`. + +The `...memoizeOptions` rest parameters are zero or more configuration options to be passed to `memoizeFunc`. The selectors `resultFunc` is passed as the first argument to `memoize` and the `memoizeOptions` are passed as the second argument onwards: + +```ts +const customSelectorCreator = createSelectorCreator( + customMemoize, // Function to be used to memoize `resultFunc` + option1, // `option1` will be passed as second argument to `customMemoize` + option2, // `option2` will be passed as third argument to `customMemoize` + option3 // `option3` will be passed as fourth argument to `customMemoize` +) + +const customSelector = customSelectorCreator( + [inputSelector1, inputSelector2], + resultFunc // `resultFunc` will be passed as first argument to `customMemoize` +) +``` + +Internally `customSelector` calls the memoize function as follows: + +```ts +customMemoize(resultFunc, option1, option2, option3) +``` + +##### Additional Examples + +###### Customize `equalityCheck` for `defaultMemoize` + +```js +import { createSelectorCreator, defaultMemoize } from 'reselect' +import isEqual from 'lodash.isequal' + +// create a "selector creator" that uses lodash.isequal instead of === +const createDeepEqualSelector = createSelectorCreator(defaultMemoize, isEqual) + +// use the new "selector creator" to create a selector +const selectSum = createDeepEqualSelector( + [state => state.values.filter(val => val < 5)], + values => values.reduce((acc, val) => acc + val, 0) +) +``` + +###### Use memoize function from `Lodash` for an unbounded cache + +```js +import { createSelectorCreator } from 'reselect' +import memoize from 'lodash.memoize' + +const hashFn = (...args) => + args.reduce((acc, val) => acc + '-' + JSON.stringify(val), '') + +const customSelectorCreator = createSelectorCreator(memoize, hashFn) + +const selector = customSelectorCreator( + [state => state.a, state => state.b], + (a, b) => a + b +) +``` + +
+ + + +--- + + + +### createStructuredSelector({ inputSelectors }, selectorCreator = createSelector) + +
+ + + +#### Description + + + +A convenience function for a common pattern that arises when using `Reselect`. +The selector passed to a `connect` decorator often just takes the values of its `input selectors` +and maps them to keys in an object. + +
+ +
+ + + +#### Parameters + + + +| Name | Description | +| :----------------- | :------------------------------------------------------------------- | +| `selectorMap` | A key value pair consisting of input selectors. | +| `selectorCreator?` | A custom selector creator function. It defaults to `createSelector`. | + +
+ +
+ + + +#### Returns + + + +A memoized structured selector. + +
+ +
+ + + +#### Type parameters + + + +| Name | Description | +| :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `InputSelectorsObject` | The shape of the `input selectors` object. | +| `MemoizeFunction` | The type of the memoize function that is used to create the structured selector. It defaults to `defaultMemoize`. | +| `ArgsMemoizeFunction` | The type of the of the memoize function that is used to memoize the arguments passed into the generated structured selector. It defaults to `defaultMemoize`. | + +
+ +
+ + + +#### **`Examples`** + + + +##### Modern Use Case + +```ts +import { createSelector, createStructuredSelector } from 'reselect' + +interface RootState { + todos: { id: number; completed: boolean }[] + alerts: { id: number; read: boolean }[] +} + +const state: RootState = { + todos: [ + { id: 0, completed: false }, + { id: 1, completed: true } + ], + alerts: [ + { id: 0, read: false }, + { id: 1, read: true } + ] +} + +// This: +const structuredSelector = createStructuredSelector( + { + allTodos: (state: RootState) => state.todos, + allAlerts: (state: RootState) => state.alerts, + selectedTodo: (state: RootState, id: number) => state.todos[id] + }, + createSelector +) + +// Is essentially the same as this: +const selector = createSelector( + [ + (state: RootState) => state.todos, + (state: RootState) => state.alerts, + (state: RootState, id: number) => state.todos[id] + ], + (allTodos, allAlerts, selectedTodo) => { + return { + allTodos, + allAlerts, + selectedTodo + } + } +) +``` + +##### Simple Use Case + +```ts +const selectA = state => state.a +const selectB = state => state.b + +// The result function in the following selector +// is simply building an object from the input selectors +const structuredSelector = createSelector(selectA, selectB, (a, b) => ({ + a, + b +})) + +const result = structuredSelector({ a: 1, b: 2 }) // will produce { x: 1, y: 2 } +``` + +
+ + + +--- + + + +## Debugging Tools + +
+ + + + + +### Development-only Checks - (**`Since`** 5.0.0) + + + + + +#### `inputStabilityCheck` + +Due to how [`Cascading Memoization`](#cascadingmemoization) works in `Reselect`, it is crucial that your `input selectors` do not return a new reference on each run. If an `input selector` always returns a new reference, like + +```ts +state => ({ a: state.a, b: state.b }) +``` + +or + +```ts +state => state.todos.map(todo => todo.id) +``` + +that will cause the selector to never memoize properly. +Since this is a common mistake, we've added a development mode check to catch this. By default, `createSelector` will now run the `input selectors` twice during the first call to the selector. If the result appears to be different for the same call, it will log a warning with the arguments and the two different sets of extracted input values. + +```ts +type StabilityCheckFrequency = 'always' | 'once' | 'never' +``` + +| Possible Values | Description | +| :-------------- | :---------------------------------------------- | +| `once` | Run only the first time the selector is called. | +| `always` | Run every time the selector is called. | +| `never` | Never run the `input stability check`. | + +> [!IMPORTANT] +> The `input stability check` is automatically disabled in production environments. + +You can configure this behavior in two ways: + + + +##### 1. Globally through `setInputStabilityCheckEnabled`: + +A `setInputStabilityCheckEnabled` function is exported from `Reselect`, which should be called with the desired setting. + +```ts +import { setInputStabilityCheckEnabled } from 'reselect' + +// Run only the first time the selector is called. (default) +setInputStabilityCheckEnabled('once') + +// Run every time the selector is called. +setInputStabilityCheckEnabled('always') + +// Never run the input stability check. +setInputStabilityCheckEnabled('never') +``` + +##### 2. Per selector by passing an `inputStabilityCheck` option directly to `createSelector`: -The **Redux docs usage page on [Deriving Data with Selectors](https://redux.js.org/usage/deriving-data-selectors)** covers the purpose and motivation for selectors, why memoized selectors are useful, typical Reselect usage patterns, and using selectors with React-Redux. +```ts +// Create a selector that double-checks the results of `input selectors` every time it runs. +const selectCompletedTodosLength = createSelector( + [ + // This `input selector` will not be memoized properly since it always returns a new reference. + (state: RootState) => + state.todos.filter(({ completed }) => completed === true) + ], + completedTodos => completedTodos.length, + // Will override the global setting. + { inputStabilityCheck: 'always' } +) +``` -[![GitHub Workflow Status][build-badge]][build] -[![npm package][npm-badge]][npm] -[![Coveralls][coveralls-badge]][coveralls] +> [!WARNING] +> This will override the global `input stability check` set by calling `setInputStabilityCheckEnabled`. -## Installation +
-### Redux Toolkit +
-While Reselect is not exclusive to Redux, it is already included by default in [the official Redux Toolkit package](https://redux-toolkit.js.org) - no further installation needed. + -```js -import { createSelector } from '@reduxjs/toolkit' -``` + -### Standalone +### Output Selector Fields -For standalone usage, install the `reselect` package: + -```bash -npm install reselect +The `output selectors` created by `createSelector` have several additional properties attached to them: -yarn add reselect -``` +| Name | Description | +| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `resultFunc` | The final function passed to `createSelector`. Otherwise known as the `combiner`. | +| `memoizedResultFunc` | The memoized version of `resultFunc`. | +| `lastResult` | Returns The last result calculated by `memoizedResultFunc`. | +| `dependencies` | The array of the input selectors used by `createSelector` to compose the combiner (`memoizedResultFunc`). | +| `recomputations` | Counts the number of times `memoizedResultFunc` has been recalculated. | +| `resetRecomputations` | Resets the count of `recomputations` count to 0. | +| `dependencyRecomputations` | Counts the number of times the input selectors (`dependencies`) have been recalculated. This is distinct from `recomputations`, which tracks the recalculations of the result function. | +| `resetDependencyRecomputations` | Resets the `dependencyRecomputations` count to 0. | +| `memoize` | Function used to memoize the `resultFunc`. | +| `argsMemoize` | Function used to memoize the arguments passed into the `output selector`. | -## Basic Usage +
-Reselect exports a `createSelector` API, which generates memoized selector functions. `createSelector` accepts one or more "input" selectors, which extract values from arguments, and an "output" selector that receives the extracted values and should return a derived value. If the generated selector is called multiple times, the output will only be recalculated when the extracted values have changed. + -You can play around with the following **example** in [this CodeSandbox](https://codesandbox.io/s/objective-waterfall-1z5y8?file=/src/index.js): +--- -```js -import { createSelector } from 'reselect' +
-const selectShopItems = state => state.shop.items -const selectTaxPercent = state => state.shop.taxPercent + -const selectSubtotal = createSelector(selectShopItems, items => - items.reduce((subtotal, item) => subtotal + item.value, 0) -) + -const selectTax = createSelector( - selectSubtotal, - selectTaxPercent, - (subtotal, taxPercent) => subtotal * (taxPercent / 100) -) +## What's New in 5.0.0? -const selectTotal = createSelector( - selectSubtotal, - selectTax, - (subtotal, tax) => ({ total: subtotal + tax }) -) + -const exampleState = { - shop: { - taxPercent: 8, - items: [ - { name: 'apple', value: 1.2 }, - { name: 'orange', value: 0.95 } - ] - } -} +Version 5.0.0 introduces several new features and improvements: -console.log(selectSubtotal(exampleState)) // 2.15 -console.log(selectTax(exampleState)) // 0.172 -console.log(selectTotal(exampleState)) // { total: 2.322 } -``` +- **Customization Enhancements**: -## Table of Contents + - Added the ability to pass an options object to `createSelectorCreator`, allowing for customized `memoize` and `argsMemoize` functions, alongside their respective options (`memoizeOptions` and `argsMemoizeOptions`). + - The `createSelector` function now supports direct customization of `memoize` and `argsMemoize` within its options object. -- [Installation](#installation) - - [Redux Toolkit](#redux-toolkit) - - [Standalone](#standalone) -- [Basic Usage](#basic-usage) -- [API](#api) - - [createSelector(...inputSelectors | [inputSelectors], resultFunc, selectorOptions?)](#createselectorinputselectors--inputselectors-resultfunc-selectoroptions) - - [defaultMemoize(func, equalityCheckOrOptions = defaultEqualityCheck)](#defaultmemoizefunc-equalitycheckoroptions--defaultequalitycheck) - - [createSelectorCreator(memoize, ...memoizeOptions)](#createselectorcreatormemoize-memoizeoptions) - - [Customize `equalityCheck` for `defaultMemoize`](#customize-equalitycheck-for-defaultmemoize) - - [Use memoize function from Lodash for an unbounded cache](#use-memoize-function-from-lodash-for-an-unbounded-cache) - - [createStructuredSelector({inputSelectors}, selectorCreator = createSelector)](#createstructuredselectorinputselectors-selectorcreator--createselector) -- [Development-only checks](#development-only-checks) - - [`inputStabilityCheck`](#inputstabilitycheck) - - [Global configuration](#global-configuration) - - [Per-selector configuration](#per-selector-configuration) -- [FAQ](#faq) - - [Q: Why isn’t my selector recomputing when the input state changes?](#q-why-isnt-my-selector-recomputing-when-the-input-state-changes) - - [Q: Why is my selector recomputing when the input state stays the same?](#q-why-is-my-selector-recomputing-when-the-input-state-stays-the-same) - - [Q: Can I use Reselect without Redux?](#q-can-i-use-reselect-without-redux) - - [Q: How do I create a selector that takes an argument?](#q-how-do-i-create-a-selector-that-takes-an-argument) - - [Q: The default memoization function is no good, can I use a different one?](#q-the-default-memoization-function-is-no-good-can-i-use-a-different-one) - - [Q: How do I test a selector?](#q-how-do-i-test-a-selector) - - [Q: Can I share a selector across multiple component instances?](#q-can-i-share-a-selector-across-multiple-component-instances) - - [Q: Are there TypeScript Typings?](#q-are-there-typescript-typings) - - [Q: How can I make a curried selector?](#q-how-can-i-make-a-curried-selector) -- [Related Projects](#related-projects) - - [re-reselect](#re-reselect) - - [reselect-tools](#reselect-tools) - - [reselect-debugger](#reselect-debugger) -- [License](#license) -- [Prior Art and Inspiration](#prior-art-and-inspiration) +- **Memoization Functions**: -## API + - Introduced new experimental memoization functions: `weakMapMemoize` and `autotrackMemoize`. + - Incorporated `memoize` and `argsMemoize` into the [`output selector fields`](#outputselectorfields) for debugging purposes. -### createSelector(...inputSelectors | [inputSelectors], resultFunc, selectorOptions?) +- **TypeScript Support and Performance**: -Accepts one or more "input selectors" (either as separate arguments or a single array), a single "output selector" / "result function", and an optional options object, and generates a memoized selector function. + - Discontinued support for `TypeScript` versions below 4.7, aligning with modern `TypeScript` features. + - Significantly improved `TypeScript` performance for nesting `output selectors`. The nesting limit has increased from approximately 8 to around 30 `output selectors`, greatly reducing the occurrence of the infamous `Type instantiation is excessively deep and possibly infinite` error. -When the selector is called, each input selector will be called with all of the provided arguments. The extracted values are then passed as separate arguments to the output selector, which should calculate and return a final result. The inputs and result are cached for later use. +- **Selector API Enhancements**: -If the selector is called again with the same arguments, the previously cached result is returned instead of recalculating a new result. + - Introduced experimental APIs: `createCurriedSelector` and `createCurriedSelectorCreator`, for more advanced selector patterns. + - Removed the second overload of `createStructuredSelector` due to its susceptibility to runtime errors. + - Added the `TypedStructuredSelectorCreator` utility type (_currently a work-in-progress_) to facilitate the creation of a pre-typed version of `createStructuredSelector` for your root state. -`createSelector` determines if the value returned by an input-selector has changed between calls using reference equality (`===`). Inputs to selectors created with `createSelector` should be immutable. +- **Additional Functionalities**: -By default, selectors created with `createSelector` have a cache size of 1. This means they always recalculate when the value of an input-selector changes, as a selector only stores the preceding value of each input-selector. This can be customized by passing a `selectorOptions` object with a `memoizeOptions` field containing options for the built-in `defaultMemoize` memoization function . + - Added `dependencyRecomputations` and `resetDependencyRecomputations` to the `output selector fields`. These additions provide greater control and insight over `input selectors`, complementing the new `argsMemoize` API. + - Introduced `inputStabilityCheck`, a development tool that runs the `input selectors` twice using the same arguments and triggers a warning If they return differing results for the same call. -```js -const selectValue = createSelector( - state => state.values.value1, - state => state.values.value2, - (value1, value2) => value1 + value2 -) +These updates aim to enhance flexibility, performance, and developer experience. For detailed usage and examples, refer to the updated documentation sections for each feature. -// You can also pass an array of selectors -const selectTotal = createSelector( - [state => state.values.value1, state => state.values.value2], - (value1, value2) => value1 + value2 -) +
-// Selector behavior can be customized -const customizedSelector = createSelector( - state => state.a, - state => state.b, - (a, b) => a + b, - { - // New in 4.1: Pass options through to the built-in `defaultMemoize` function - memoizeOptions: { - equalityCheck: (a, b) => a === b, - maxSize: 10, - resultEqualityCheck: shallowEqual - } + + +--- + +
+ + + + + +## Optimizing `Reselect` + + + +### Empty Array Pattern + +To reduce recalculations, use a predefined empty array when `array.filter` or similar methods result in an empty array. + +So you can have a pattern like this: + +```ts +const EMPTY_ARRAY = [] + +const selectCompletedTodos = createSelector( + [(state: RootState) => state.todos], + todos => { + const completedTodos = todos.filter(todo => todo.completed === true) + return completedTodos.length === 0 ? EMPTY_ARRAY : completedTodos } ) ``` -Selectors are typically called with a Redux `state` value as the first argument, and the input selectors extract pieces of the `state` object for use in calculations. However, it's also common to want to pass additional arguments, such as a value to filter by. Since input selectors are given all arguments, they can extract the additional arguments and pass them to the output selector: +Or to avoid repetition, you can create a wrapper function and reuse it: -```js -const selectItemsByCategory = createSelector( - [ - // Usual first input - extract value from `state` - state => state.items, - // Take the second arg, `category`, and forward to the output selector - (state, category) => category - ], - // Output selector gets (`items, category)` as args - (items, category) => items.filter(item => item.category === category) +```ts +const EMPTY_ARRAY = [] + +export const fallbackToEmptyArray = (array: T[]) => { + return array.length === 0 ? EMPTY_ARRAY : array +} + +const selectCompletedTodos = createSelector( + [(state: RootState) => state.todos], + todos => { + return fallbackToEmptyArray(todos.filter(todo => todo.completed === true)) + } ) ``` -### defaultMemoize(func, equalityCheckOrOptions = defaultEqualityCheck) - -`defaultMemoize` memoizes the function passed in the func parameter. It is the standard memoize function used by `createSelector`. +There are a few details that will help you skip running as many functions as possible and get the best possible performance out of `Reselect`: -`defaultMemoize` has a default cache size of 1. This means it always recalculates when the value of an argument changes. However, this can be customized as needed with a specific max cache size (new in 4.1). +- Due to the `Cascading Memoization` in `Reselect`, The first layer of checks is upon the arguments that are passed to the `output selector`, therefore it's best to maintain the same reference for the arguments as much as possible. +- In `Redux`, your state will change reference when updated. But it's best to keep the additional arguments as simple as possible, try to avoid passing in objects or arrays and mostly stick to primitives like numbers for ids. +- Keep your [`input selectors`](#input-selectors) as simple as possible. It's best if they mostly consist of field accessors like `state => state.todos` or argument providers like `(state, id) => id`. You should not be doing any sort of calculation inside `input selectors`, and you should definitely not be returning an object or array with a new reference each time. +- The `result function` is only re-run as a last resort. So make sure to put any and all calculations inside your `result function`. That way, `Reselect` will only run those calculations if all other checks fail. -`defaultMemoize` determines if an argument has changed by calling the `equalityCheck` function. As `defaultMemoize` is designed to be used with immutable data, the default `equalityCheck` function checks for changes using reference equality: +This: -```js -function defaultEqualityCheck(previousVal, currentVal) { - return currentVal === previousVal -} +```ts +const selectorGood = createSelector( + [(state: RootState) => state.todos], + todos => someExpensiveComputation(todos) +) ``` -As of Reselect 4.1, `defaultMemoize` also accepts an options object as its first argument instead of `equalityCheck`. The options object may contain: +Is preferable to this: ```ts -interface DefaultMemoizeOptions { - equalityCheck?: EqualityFn - resultEqualityCheck?: EqualityFn - maxSize?: number -} +const selectorBad = createSelector( + [(state: RootState) => someExpensiveComputation(state.todos)], + someOtherCalculation +) ``` -Available options are: +Because we have less calculations in input selectors and more in the result function. -- `equalityCheck`: used to compare the individual arguments of the provided calculation function -- `resultEqualityCheck`: if provided, used to compare a newly generated output value against previous values in the cache. If a match is found, the old value is returned. This address the common `todos.map(todo => todo.id)` use case, where an update to another field in the original data causes a recalculate due to changed references, but the output is still effectively the same. -- `maxSize`: the cache size for the selector. If `maxSize` is greater than 1, the selector will use an LRU cache internally +
-The returned memoized function will have a `.clearCache()` method attached. + -`defaultMemoize` can also be used with `createSelectorCreator` to create a new selector factory that always has the same settings for each selector. +--- -### createSelectorCreator(memoize, ...memoizeOptions) +## FAQ -`createSelectorCreator` can be used to make a customized version of `createSelector`. +
-The `memoize` argument is a memoization function to replace `defaultMemoize`. + -The `...memoizeOptions` rest parameters are zero or more configuration options to be passed to `memoizeFunc`. The selectors `resultFunc` is passed as the first argument to `memoize` and the `memoizeOptions` are passed as the second argument onwards: +### Why isn’t my selector recomputing when the input state changes? -```js -const customSelectorCreator = createSelectorCreator( - customMemoize, // function to be used to memoize resultFunc - option1, // option1 will be passed as second argument to customMemoize - option2, // option2 will be passed as third argument to customMemoize - option3 // option3 will be passed as fourth argument to customMemoize -) + -const customSelector = customSelectorCreator( - input1, - input2, - resultFunc // resultFunc will be passed as first argument to customMemoize -) -``` +A: Check that your memoization function is compatible with your state update function (i.e. the reducer if you are using `Redux`). For example, a selector created with `createSelector` will not work with a state update function that mutates an existing object instead of creating a new one each time. `createSelector` uses an identity check (`===`) to detect that an input has changed, so mutating an existing object will not trigger the selector to recompute because mutating an object does not change its identity. Note that if you are using `Redux`, mutating the state object is [almost certainly a mistake](http://redux.js.org/docs/Troubleshooting.html). -Internally `customSelector` calls the memoize function as follows: +
-```js -customMemoize(resultFunc, option1, option2, option3) -``` +
-Here are some examples of how you might use `createSelectorCreator`: + -#### Customize `equalityCheck` for `defaultMemoize` +### Why is my selector recomputing when the input state stays the same? -```js -import { createSelectorCreator, defaultMemoize } from 'reselect' -import isEqual from 'lodash.isequal' +A: Make sure you have `inputStabilityCheck` set to either `always` or `once` and that in and of itself should take some weight off of your shoulders by doing some of the debugging for you. Also make sure you use your `output selector fields` like `recomputations`, `resetRecomputations`, `dependencyRecomputations`, `resetDependencyRecomputations` to narrow down the root of the problem to see where it is coming from. Is it coming from the arguments changing reference unexpectedly? then if that is the case your `dependencyRecomputations` should be going up. If `dependencyRecomputations` is incrementing but `recomputations` is not, that means your arguments are changing reference too often. If your `input selectors` return a new reference every time, that will be caught be `inputStabilityCheck`. And if your arguments are changing reference too often, you can narrow it down to see which arguments are changing reference too often by doing this: -// create a "selector creator" that uses lodash.isequal instead of === -const createDeepEqualSelector = createSelectorCreator(defaultMemoize, isEqual) +```ts +interface RootState { + todos: { id: number; completed: boolean }[] + alerts: { id: number; read: boolean; type: string }[] +} -// use the new "selector creator" to create a selector -const selectSum = createDeepEqualSelector( - state => state.values.filter(val => val < 5), - values => values.reduce((acc, val) => acc + val, 0) +const selectAlertsByType = createSelector( + [ + (state: RootState) => state.alerts, + (state: RootState, type: string) => type + ], + (alerts, type) => alerts.filter(todo => todo.type === type), + { + argsMemoizeOptions: { + // This will check the arguments passed to the `output selector`. + equalityCheck: (a, b) => { + if (a !== b) { + console.log(a, 'is not equal to', b) + } + return a === b + } + } + } ) ``` -#### Use memoize function from Lodash for an unbounded cache + -```js -import { createSelectorCreator } from 'reselect' -import memoize from 'lodash.memoize' +
-let called = 0 -const hashFn = (...args) => - args.reduce((acc, val) => acc + '-' + JSON.stringify(val), '') -const customSelectorCreator = createSelectorCreator(memoize, hashFn) -const selector = customSelectorCreator( - state => state.a, - state => state.b, - (a, b) => { - called++ - return a + b - } -) -``` +
-### createStructuredSelector({inputSelectors}, selectorCreator = createSelector) + -`createStructuredSelector` is a convenience function for a common pattern that arises when using Reselect. The selector passed to a `connect` decorator often just takes the values of its input-selectors and maps them to keys in an object: +### Can I use `Reselect` without `Redux`? -```js -const selectA = state => state.a -const selectB = state => state.b + -// The result function in the following selector -// is simply building an object from the input selectors -const structuredSelector = createSelector(selectA, selectB, (a, b) => ({ - a, - b -})) -``` +A: Yes. `Reselect` has no dependencies on any other package, so although it was designed to be used with `Redux` it can be used independently. It can be used with any plain JS data, such as typical `React` state values, as long as that data is being updated immutably. -`createStructuredSelector` takes an object whose properties are input-selectors and returns a structured selector. The structured selector returns an object with the same keys as the `inputSelectors` argument, but with the selectors replaced with their values. +
-```js -const selectA = state => state.a -const selectB = state => state.b +
-const structuredSelector = createStructuredSelector({ - x: selectA, - y: selectB -}) + -const result = structuredSelector({ a: 1, b: 2 }) // will produce { x: 1, y: 2 } -``` +### How do I create a selector that takes an argument? -Structured selectors can be nested: +When creating a selector that accepts arguments in `Reselect`, it's important to structure your input and `output selectors` appropriately. Here are key points to consider: -```js -const nestedSelector = createStructuredSelector({ - subA: createStructuredSelector({ - selectorA, - selectorB - }), - subB: createStructuredSelector({ - selectorC, - selectorD - }) -}) -``` +1. **Consistency in Arguments**: Ensure that all positional arguments across `input selectors` are of the same type for consistency. -## Development-only checks +2. **Selective Argument Usage**: Design each selector to use only its relevant argument(s) and ignore the rest. This is crucial because all `input selectors` receive the same arguments that are passed to the `output selector`. -### `inputStabilityCheck` + + +Suppose we have the following state structure: + +```ts +interface RootState { + items: { id: number; category: string }[] + // ... other state properties ... +} ``` -By default, this will only happen when the selector is first called. You can configure the check globally or per selector, to change it to always run when the selector is called, or to never run. +To create a selector that filters `items` based on a `category` and excludes a specific `id`, you can set up your selectors as follows: -_This check is disabled for production environments._ +```ts +const selectAvailableItems = createSelector( + [ + // First input selector extracts items from the state + (state: RootState) => state.items, + // Second input selector forwards the category argument + (state: RootState, category: string) => category, + // Third input selector forwards the ID argument + (state: RootState, category: string, id: number) => id + ], + // Output selector uses the extracted items, category, and ID + (items, category, id) => + items.filter(item => item.category === category && item.id !== id) +) +``` -#### Global configuration +Internally `Reselect` is doing this: -A `setInputStabilityCheckEnabled` function is exported from `reselect`, which should be called with the desired setting. +```ts +// Input selector #1 +const items = (state: RootState, category: string, id: number) => state.items +// Input selector #2 +const category = (state: RootState, category: string, id: number) => category +// Input selector #3 +const id = (state: RootState, category: string, id: number) => id +// result of `output selector` +const finalResult = + // The `Result Function` + items.filter(item => item.category === category && item.id !== id) +``` -```js -import { setInputStabilityCheckEnabled } from 'reselect' + -// run when selector is first called (default) -setInputStabilityCheckEnabled('once') +
-// always run -setInputStabilityCheckEnabled('always') +
-// never run -setInputStabilityCheckEnabled('never') -``` + -#### Per-selector configuration +### The default memoization function is no good, can I use a different one? -A value can be passed as part of the selector options object, which will override the global setting for the given selector. + -```ts -const selectPersonName = createSelector( - selectPerson, - person => person.firstName + ' ' + person.lastName, - // `inputStabilityCheck` accepts the same settings - // as `setInputStabilityCheckEnabled` - { inputStabilityCheck: 'never' } -) -``` +A: We think it works great for a lot of use cases, but sure. See [these examples](#customize-equalitycheck-for-defaultmemoize). -## FAQ +
-### Q: Why isn’t my selector recomputing when the input state changes? +
-A: Check that your memoization function is compatible with your state update function (i.e. the reducer if you are using Redux). For example, a selector created with `createSelector` will not work with a state update function that mutates an existing object instead of creating a new one each time. `createSelector` uses an identity check (`===`) to detect that an input has changed, so mutating an existing object will not trigger the selector to recompute because mutating an object does not change its identity. Note that if you are using Redux, mutating the state object is [almost certainly a mistake](http://redux.js.org/docs/Troubleshooting.html). + -The following example defines a simple selector that determines if the first todo item in an array of todos has been completed: +### How do I test a selector? -```js -const selectIsFirstTodoComplete = createSelector( - state => state.todos[0], - todo => todo && todo.completed -) -``` +A: For a given input, a selector should always produce the same result. For this reason they are simple to unit test. -The following state update function **will not** work with `selectIsFirstTodoComplete`: +```ts +interface RootState { + todos: { id: number; completed: boolean }[] + alerts: { id: number; read: boolean }[] +} -```js -export default function todos(state = initialState, action) { - switch (action.type) { - case COMPLETE_ALL: - const areAllMarked = state.every(todo => todo.completed) - // BAD: mutating an existing object - return state.map(todo => { - todo.completed = !areAllMarked - return todo - }) - - default: - return state - } +const state: RootState = { + todos: [ + { id: 0, completed: false }, + { id: 1, completed: true } + ], + alerts: [ + { id: 0, read: false }, + { id: 1, read: true } + ] } + +// With `Vitest` +test('selector unit test', () => { + const selectTodoIds = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id) + ) + const firstResult = selectTodoIds(state) + const secondResult = selectTodoIds(state) + // Reference equality should pass. + expect(firstResult).toBe(secondResult) + // Deep equality should also pass. + expect(firstResult).toStrictEqual(secondResult) + selectTodoIds(state) + selectTodoIds(state) + selectTodoIds(state) + // The `Result Function` should not recalculate. + expect(selectTodoIds.recomputations()).toBe(1) + // `Input selectors` should not recalculate. + expect(selectTodoIds.dependencyRecomputations()).toBe(1) +}) ``` -The following state update function **will** work with `selectIsFirstTodoComplete`: + -```js -export default function todos(state = initialState, action) { - switch (action.type) { - case COMPLETE_ALL: - const areAllMarked = state.every(todo => todo.completed) - // GOOD: returning a new object each time with Object.assign - return state.map(todo => - Object.assign({}, todo, { - completed: !areAllMarked - }) - ) - - default: - return state - } -} -``` +
-If you are not using Redux and have a requirement to work with mutable data, you can use `createSelectorCreator` to replace the default memoization function and/or use a different equality check function. See [here](#use-memoize-function-from-lodash-for-an-unbounded-cache) and [here](#customize-equalitycheck-for-defaultmemoize) for examples. +
-### Q: Why is my selector recomputing when the input state stays the same? + -A: Check that your memoization function is compatible with your state update function (i.e. the reducer if you are using Redux). For example, a selector created with `createSelector` that recomputes unexpectedly may be receiving a new object on each update whether the values it contains have changed or not. `createSelector` uses an identity check (`===`) to detect that an input has changed, so returning a new object on each update means that the selector will recompute on each update. +### Can I share a selector across multiple component instances? -```js -import { REMOVE_OLD } from '../constants/ActionTypes' + -const initialState = [ - { - text: 'Use Redux', - completed: false, - id: 0, - timestamp: Date.now() - } -] - -export default function todos(state = initialState, action) { - switch (action.type) { - case REMOVE_OLD: - return state.filter(todo => { - return todo.timestamp + 30 * 24 * 60 * 60 * 1000 > Date.now() - }) - default: - return state - } -} -``` +A: Yes, as of 5.0.0 you can use `weakMapMemoize` to achieve this. -The following selector is going to recompute every time REMOVE_OLD is invoked because Array.filter always returns a new object. However, in the majority of cases the REMOVE_OLD action will not change the list of todos so the recomputation is unnecessary. +
-```js -import { createSelector } from 'reselect' +
-const todosSelector = state => state.todos + -export const selectVisibleTodos = createSelector( - todosSelector, - (todos) => { - ... - } -) -``` +### Are there TypeScript Typings? -You can eliminate unnecessary recomputations by returning a new object from the state update function only when a deep equality check has found that the list of todos has actually changed: + -```js -import { REMOVE_OLD } from '../constants/ActionTypes' -import isEqual from 'lodash.isequal' +A: Yes! `Reselect` is now written in `TypeScript` itself, so they should Just Work™. -const initialState = [ - { - text: 'Use Redux', - completed: false, - id: 0, - timestamp: Date.now() - } -] - -export default function todos(state = initialState, action) { - switch (action.type) { - case REMOVE_OLD: - const updatedState = state.filter(todo => { - return todo.timestamp + 30 * 24 * 60 * 60 * 1000 > Date.now() - }) - return isEqual(updatedState, state) ? state : updatedState - default: - return state - } -} -``` +
-Alternatively, the default `equalityCheck` function in the selector can be replaced by a deep equality check: +
-```js -import { createSelectorCreator, defaultMemoize } from 'reselect' -import isEqual from 'lodash.isequal' + -const selectTodos = state => state.todos +### I am seeing a TypeScript error: `Type instantiation is excessively deep and possibly infinite` -// create a "selector creator" that uses lodash.isequal instead of === -const createDeepEqualSelector = createSelectorCreator( - defaultMemoize, - isEqual -) + -// use the new "selector creator" to create a selector -const mySelector = createDeepEqualSelector( - todosSelector, - (todos) => { - ... - } -) -``` +A: **`Since`** 5.0.0 you should be able to nest up to 30 selectors, but in case you still run into this issue, you can refer to [this +comment](https://github.com/reduxjs/reselect/issues/534#issuecomment-956708953) for a discussion of the problem, as +relating to nested selectors. -Always check that the cost of an alternative `equalityCheck` function or deep equality check in the state update function is not greater than the cost of recomputing every time. If recomputing every time does work out to be the cheaper option, it may be that for this case Reselect is not giving you any benefit over passing a plain `mapStateToProps` function to `connect`. +
-### Q: Can I use Reselect without Redux? +
-A: Yes. Reselect has no dependencies on any other package, so although it was designed to be used with Redux it can be used independently. It can be used with any plain JS data, such as typical React state values, as long as that data is being updated immutably. + -### Q: How do I create a selector that takes an argument? +### How can I make a [curried](https://github.com/hemanth/functional-programming-jargon#currying) selector? -As shown in the API reference section above, provide input selectors that extract the arguments and forward them to the output selector for calculation: + -```js -const selectItemsByCategory = createSelector( - [ - // Usual first input - extract value from `state` - state => state.items, - // Take the second arg, `category`, and forward to the output selector - (state, category) => category - ], - // Output selector gets (`items, category)` as args - (items, category) => items.filter(item => item.category === category) -) -``` +A: You can try this new experimental API: -### Q: The default memoization function is no good, can I use a different one? + -A: We think it works great for a lot of use cases, but sure. See [these examples](#customize-equalitycheck-for-defaultmemoize). +#### createCurriedSelector(...inputSelectors | [inputSelectors], resultFunc, createSelectorOptions?) -### Q: How do I test a selector? +This: + +```ts +const parametricSelector = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.filter(todo => todo.id === id) +) -A: For a given input, a selector should always produce the same output. For this reason they are simple to unit test. +parametricSelector(state, 0) +``` -```js -const selector = createSelector( - state => state.a, - state => state.b, - (a, b) => ({ - c: a * 2, - d: b * 3 - }) +Is the same as this: + +```ts +const curriedSelector = createCurriedSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.filter(todo => todo.id === id) ) -test('selector unit test', () => { - assert.deepEqual(selector({ a: 1, b: 2 }), { c: 2, d: 6 }) - assert.deepEqual(selector({ a: 2, b: 3 }), { c: 4, d: 9 }) -}) +curriedSelector(0)(state) ``` -It may also be useful to check that the memoization function for a selector works correctly with the state update function (i.e. the reducer if you are using Redux). Each selector has a `recomputations` method that will return the number of times it has been recomputed: +As before you had to do this: -```js -suite('selector', () => { - let state = { a: 1, b: 2 } - - const reducer = (state, action) => ({ - a: action(state.a), - b: action(state.b) - }) - - const selector = createSelector( - state => state.a, - state => state.b, - (a, b) => ({ - c: a * 2, - d: b * 3 - }) - ) +```ts +const selectTodo = useSelector(state => parametricSelector(state, props.id)) +``` - const plusOne = x => x + 1 - const id = x => x - - test('selector unit test', () => { - state = reducer(state, plusOne) - assert.deepEqual(selector(state), { c: 4, d: 9 }) - state = reducer(state, id) - assert.deepEqual(selector(state), { c: 4, d: 9 }) - assert.equal(selector.recomputations(), 1) - state = reducer(state, plusOne) - assert.deepEqual(selector(state), { c: 6, d: 12 }) - assert.equal(selector.recomputations(), 2) - }) -}) +Now you can do this: + +```ts +const selectTodo = useSelector(curriedSelector(props.id)) ``` -Additionally, selectors keep a reference to the last result function as `.resultFunc`. If you have selectors composed of many other selectors this can help you test each selector without coupling all of your tests to the shape of your state. +Another thing you can do if you are using `React-Redux` is this: -For example if you have a set of selectors like this: +```ts +import type { GetParamsFromSelectors, Selector } from 'reselect' +import { useSelector } from 'react-redux' + +export const createParametricSelectorHook = ( + selector: S +) => { + return (...args: GetParamsFromSelectors<[S]>) => { + return useSelector(state => selector(state, ...args)) + } +} -**selectors.js** +const useSelectTodo = createParametricSelectorHook(parametricSelector) +``` -```js -export const selectFirst = createSelector( ... ) -export const selectSecond = createSelector( ... ) -export const selectThird = createSelector( ... ) +And then inside your component: -export const myComposedSelector = createSelector( - selectFirst, - selectSecond, - selectThird, - (first, second, third) => first * second < third -) +```tsx +import type { FC } from 'react' + +interface Props { + id: number +} + +const MyComponent: FC = ({ id }) => { + const todo = useSelectTodo(id) + return
{todo.title}
+} ``` -And then a set of unit tests like this: +### How can I make pre-typed version of `createSelector` for my root state? -**test/selectors.js** +A: You can create a custom typed version of `createSelector` by defining a utility type that extends the original `createSelector` function. Here's an example: -```js -// tests for the first three selectors... -test("selectFirst unit test", () => { ... }) -test("selectSecond unit test", () => { ... }) -test("selectThird unit test", () => { ... }) - -// We have already tested the previous -// three selector outputs so we can just call `.resultFunc` -// with the values we want to test directly: -test("myComposedSelector unit test", () => { - // here instead of calling selector() - // we just call selector.resultFunc() - assert(myComposedSelector.resultFunc(1, 2, 3), true) - assert(myComposedSelector.resultFunc(2, 2, 1), false) -}) +```ts +import type { createSelector, SelectorsArray, Selector } from 'reselect' + +interface RootState { + todos: { id: number; completed: boolean }[] + alerts: { id: number; read: boolean }[] +} + +export type TypedCreateSelector = < + SelectorsArray extends readonly Selector[], + Result +>( + ...createSelectorArgs: Parameters< + typeof createSelector + > +) => ReturnType> + +export const createAppSelector: TypedCreateSelector = createSelector ``` -Finally, each selector has a `resetRecomputations` method that sets -recomputations back to 0. The intended use is for a complex selector that may -have many independent tests and you don't want to manually manage the -computation count or create a "dummy" selector for each test. +> [!WARNING]: This approach currently only supports `input selectors` provided as a single array. -### Q: Can I share a selector across multiple component instances? +
-A: Yes, although it requires some planning. + -As of Reselect 4.1, you can create a selector with a cache size greater than one by passing in a `maxSize` option under `memoizeOptions` for use with the built-in `defaultMemoize`. +--- -Otherwise, selectors created using `createSelector` only have a cache size of one. This can make them unsuitable for sharing across multiple instances if the arguments to the selector are different for each instance of the component. There are a couple of ways to get around this: +
-- Create a factory function which returns a new selector for each instance of the component. This can be called in a React component inside the `useMemo` hook to generate a unique selector instance per component. -- Create a custom selector with a cache size greater than one using `createSelectorCreator` + -### Q: Are there TypeScript Typings? +## External References -A: Yes! Reselect is now written in TS itself, so they should Just Work™. + -### Q: I am seeing a TypeScript error: `Type instantiation is excessively deep and possibly infinite` +- [**`WeakMap`**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) +- [**`Reference Equality Check`**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality) -A: This can often occur with deeply recursive types, which occur in this library. Please see [this -comment](https://github.com/reduxjs/reselect/issues/534#issuecomment-956708953) for a discussion of the problem, as -relating to nested selectors. +
-### Q: How can I make a [curried](https://github.com/hemanth/functional-programming-jargon#currying) selector? +
-A: Try these [helper functions](https://github.com/reduxjs/reselect/issues/159#issuecomment-238724788) courtesy of [MattSPalmer](https://github.com/MattSPalmer) + ## Related Projects + + ### [re-reselect](https://github.com/toomuchdesign/re-reselect) -Enhances Reselect selectors by wrapping `createSelector` and returning a memoized collection of selectors indexed with the cache key returned by a custom resolver function. +Enhances `Reselect` selectors by wrapping `createSelector` and returning a memoized collection of selectors indexed with the cache key returned by a custom resolver function. Useful to reduce selectors recalculation when the same selector is repeatedly called with one/few different arguments. @@ -696,7 +1840,7 @@ Useful to reduce selectors recalculation when the same selector is repeatedly ca [Flipper plugin](https://github.com/vlanemcev/flipper-plugin-reselect-debugger) and [and the connect app](https://github.com/vlanemcev/reselect-debugger-flipper) for debugging selectors in **React Native Apps**. -Inspired by Reselect Tools, so it also has all functionality from this library and more, but only for React Native and Flipper. +Inspired by `Reselect Tools`, so it also has all functionality from this library and more, but only for React Native and Flipper. - Selectors Recomputations count in live time across the App for identify performance bottlenecks - Highlight most recomputed selectors @@ -706,17 +1850,36 @@ Inspired by Reselect Tools, so it also has all functionality from this library a - Selectors Output (In case if selector not dependent from external arguments) - Shows "Not Memoized (NM)" selectors +
+ + + +--- + ## License MIT +
+ + + ## Prior Art and Inspiration + + Originally inspired by getters in [NuclearJS](https://github.com/optimizely/nuclear-js.git), [subscriptions](https://github.com/Day8/re-frame#just-a-read-only-cursor) in [re-frame](https://github.com/Day8/re-frame) and this [proposal](https://github.com/reduxjs/redux/pull/169) from [speedskater](https://github.com/speedskater). -[build-badge]: https://img.shields.io/github/workflow/status/reduxjs/redux-thunk/Tests +[typescript-badge]: https://img.shields.io/badge/TypeScript-v4%2E7%2B-007ACC?style=for-the-badge&logo=TypeScript&logoColor=black&labelColor=blue&color=gray +[build-badge]: https://img.shields.io/github/actions/workflow/status/reduxjs/reselect/build-and-test-types.yml?branch=master&style=for-the-badge [build]: https://github.com/reduxjs/reselect/actions/workflows/build-and-test-types.yml -[npm-badge]: https://img.shields.io/npm/v/reselect.svg?style=flat-square +[npm-badge]: https://img.shields.io/npm/v/reselect.svg?style=for-the-badge [npm]: https://www.npmjs.org/package/reselect -[coveralls-badge]: https://img.shields.io/coveralls/reduxjs/reselect/master.svg?style=flat-square +[coveralls-badge]: https://img.shields.io/coveralls/reduxjs/reselect/master.svg?style=for-the-badge [coveralls]: https://coveralls.io/github/reduxjs/reselect + +
+ + + +--- diff --git a/docs/assets/diagrams/normal-memoization-function.drawio b/docs/assets/diagrams/normal-memoization-function.drawio new file mode 100644 index 000000000..479edfd6e --- /dev/null +++ b/docs/assets/diagrams/normal-memoization-function.drawio @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/diagrams/reselect-memoization.drawio b/docs/assets/diagrams/reselect-memoization.drawio new file mode 100644 index 000000000..3bdab1502 --- /dev/null +++ b/docs/assets/diagrams/reselect-memoization.drawio @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/normal-memoization-function.png b/docs/assets/normal-memoization-function.png new file mode 100644 index 0000000000000000000000000000000000000000..bffa8ede94eb8c19308bc7cf22bbdcb94084574f GIT binary patch literal 17789 zcmeIac|6qb`!CKIHIgk^QkD{mkbMh-ELkhr#+K~s2xH$ymP)8>*(p-SAjUSXnUeE-1A{d(Qkecji6Ezj$^uex_tTb1ev%MlV1 z5~?d|m#>qMK(Hhvq};GWU_|ZhOMdW&)cv}uB1u8VsaX<|Bj-Gn4LzK_ZS0+_Nw`Fm zetqH+zUXdg;bhGva+yn5SP1-pky}VND{Hr3Ll=odBFbQbmA!?nn}suYZg2JLnY);; zt&Wo&(nwOxSKUkFnwS^A`!5PE7S0DGuUgnz^Z%s#lZdmos+)zYofgu{+6hpy@;+Et zSXA`aOe>#XBjO_BzXojGelK^x9dSU{-uE}lKN%FhXp6G9vUWe9_G_jG66xe&@A}U{ zOQeg7wWY^DUbJv?Lwf!5H5;VU!6pw@bp`wQ$0Xu_?!SnRIBH;T(r(b{1AhuRnPCDOfio z5>O_dle4#ywG&aezvcF8p31-T^soK7S-TwY_CKB=@ry4{3n$cpM1Bppd-(j;BjR>k zxkMB#++Bak+Q#178f;JTFW!Z~mjgQ1ZXVXZ)!^6A!5LZH`c3aI8UD@VKUB&Z>1^%c z<^%S8KuG*x#(^@4{{HUu_tA(;3JHk)TF>s!bCEijYH^?twto=^rg0!d@Zmtp|F*=z zn?yVMLqrO0)?mQG%@*Yh#0;$DqC3$KIPlm8H>mz}+bwXUm$ zB~jj9z^z*Ez&D~Cf9eiVyg$GFL&X1b0KaIvTKp}@-^cmiAH%`URR7xkZ`lfq z{ZqKY2Veh?tMzARP(-2M zLikfyLI;BRQ&_(h_J2VOk+&R(#zxepTfn*gbm&IFCH_>!-@f3dm;I;G{-J2rR=}qn zn6V4e1*qGfdIl8N&BqWBz6i#Delhy<%VnbN{q^WFamU1O|Io*OSETMpl-p1B25%f# zj|Z?8>jRH;aPCC@09o+PzhnMx)=m~4_MU&f^wY$C&iS9PWMJ3_V*IC!ev9wGVf|OpJriLQ_wAg=B3;!*X{SRp2zeJydF#dd%Ve`A85 z0qYOs^;5gS;BQR#`}+YX6j6R-IT}Dh!a;K7vce5-vxQ*_f4#}H{qO7+TClSZgJCQj z3Ch)GRGnN+V9jcM;UU`_Fn|QkSx3YQa|a9hV33}76e;=PN0IL`3 z+vNGzu1docAka5YzXk24|FEL&KRa}GJ->6h)-0f1Z80EWed$X&GJQMMeZD$y{3FF@ zGzkR85&ZKP{4V(%EkenJH9Y*U!JAMR_7v>*8zhh{CJtP9?d7Aiq)=Ko3Gr8OSF6yk z`NT=IzNCtdGa3yfbiba0R{#OB|9=oz?un)R(#~WS*M3sGb+pVmR_QR!(_1g$Die)8 zcxU6;p^}JIHX+S$GxyS*W11&Pp>Me1c2vg7%rp?WBRl=Ob&tYm_OyN17x@$SzeV(v zBh6bnI}_i(*JM{!Ft9jwj-NmN>C-btb%8Y(e75X;R@x=3SFz>I`sH{X6mFTi=E%mp_KDJ<6b@ zhm)Y~C}5v+T!Z16haG-J*DJEJ=S)mIsj+n0MPdoN#bXgy1NHv}+$MDNxE z*oW)zrMGyOJ%~@>Kry1Mp^a{%s=kPhGdo~Ea5?t87yKvnGk|(nk~W}zvoWSHQDd9t zrl4by^n?gvX$f+gHXMe^3=U6#l0C{^$iGu_%Kr+MVamB7Qvssf4 zO$$z1>BiCUkYEEysD7cQml-f1K|B zyUSed?ZSI5ASySfQmj9}3F~>O%TpN6bkDQgY2b|4Fe>rVax>@S*jV~|LVmJpikSX4 zxEKB%W8&X+sGu=@Mz7k!}qz?W}n zYe&E~1h2++<(*#m-o&2hSXiSUxI1s2Sa{LlNt&qrcx1H)dU{JPII=@uNv8}3bwio)76G%D{X zyWG$v1A{ARQfH(4DV`e{8S%8|xKDRXPI!LmmcyB@UpmH<&qwGuHMDCmnTe^a@mo(Y zM%EB^UVWQ+7Sk%bK4kg+l;DDDleF)uorsRtpkCB#)0E3(rFo0&+@bpWL5Z_;q$%Vu zthhl&r7EuO+c z6@f+G7rqFCRx`YCu|t&|l2p*VGjpGTPqY5Y(Tv65S?W^Drf95dDIw{l%!f*{q;S54 z&YI-8y(L{N4zKjek0$R>)y|Ip387mJSJ0hFG0_HZGwF}gH(fKbLq%NnVxU)Yv}P!1 zeQl)d8InUE5*Zn(brq!_9XpI4^Yn=5OJ)lQ?MrU`E)Au9agQXcl-r^D5c)2~4GA1x z-q-W_oIf<}(IX3e{E??UY)fmtG z<(LwtMESMs2+*~3k5}*WX!WgY}>{=&%T(u%7at+8WsHc4DD^;dKxI3ztO3wWa6_3yWcB& zYw5RAW6FoY7a?ywzWa+YIC=x-;jB_=K5U0!nk=>@w6C=>h1mmh1lw_Gn6H#!VFHyt zou547(W}LSUDcnwN>Io6ap8sCS?m0i#k&bUrp1^y_jYL!v6~AMQZSB(ykOD1v-XG4 zw3Ih^aNL1IOVp)W7?(!>p&lFH@R5Yrbg&Fa5F;wa7f? zpN}8Iu>xV9@l`{Y|BXxlmUatcJSjMN- zFgP4?jS@>L)a@(==n|k4vtZL8x;bJuyoDg$Agri7)4q0+D@)9Q!Lk~5*%%nn!j_OP z^zktc9QV71*u3*_J1jS|jw%Tn0pgV7yQWCGyWt?nv7qjMI!A%MhGCvi=J*5&?X&BT z>E?VG2S26gSS<0q@Mv~kp){&iQUOV-H$b%!8&WWKWf58DR;#C3ca4vlM$z$9Uh^s1 zyU~D_bm^^Z*aySlF5aoy=l9k_P;j7tjm-?z7r&=GKUSA69v?v7&6p(yLv3BIHv?O% zDYczqq-YSr;SDa9#&xuR=i3@5nKpirAZYGV`=EZe{o3Nm-T6c)&auD4zbbB}-F&#p zD{-L2R>Q&~Yo;^u{(Y;t4=;I2V?XAKFtSbnC-GhaIs5Fnc_K@nQhXOnU+YBy=s4JV zl&xtJ1B8d7fgd;5{{&!+YixlV7Tw4Qx5AqOX4MrteEP%YItUY!)DP;I64`nR?2(tF z4LPIDVmIqa=uK1s)QRQRdW+93p}SiE zq<}BA`~o}6Pg~ygsV-oCm&JbMW3EX{uZ9Ry359Rb_N8p^GI!{USB=E6}J<*KBR)b*=ZIeK$-63HnUn>k}H-TEr3Ce7Y2Ge{;{5B^py zv1N98EplA#9wq|;iDCFv{jA2IZI0MvgecXS;NUP|FO-oTaSQZ!mF!9j9X@9W@nJs~ z(s5qq&<3h2sTW(xaTPf2c%`?LK)smqaQ8?c@j&0D#A%B_uIHfei0sLkEHM%f{N?&v z;Ei|hZ0CLeV_SGxnjyl4+j^;G4&wm6gc}DNBp+GvmL#N<0CX}QmkK_A3_%9Z)qq}L zvSbplg=kXf1t4I=)h{H^)$FK9(gf&=fYmtM{+@)F1<(B)7N#{Ruz?P}Oc0_cIQ0J> z=9Pp?Cfm|vxI{#H)31o=>X55zXyjP8B?zxn+G@%VAEsHx$+M{b&Z`YME|zJaFRguv z?J0FMptNZ_zDw)9KI}Ew^N4fOoGnGkZjCHejus9FSvV91-kxacZ_C*|+#Dq_V)xo9 z=_xw|K&p721c7+RK-9@B0*&hy$lL*nB!P&-s#m{2j3k24m+G97sCuoVda?(}$pU8L1%RWqA=O7X7s$DntN)dEF_L2P7Tc{*j`f+8f^jEw_=Q9At})q@R?JH{)Kl4?Nr=J!UMJEE4F$L0429-k1=DufEu-HJ=K8!-tt z^(&n{Q>9GVvg8(RFx)Kw>I?^U3B$7&3sjMt?=~`Cl@p#O=lbU16H!x3>y`y>ZmXo7$# zZ@-xzc4s`x$jv5V=?Qo5TwlBk+2QP`KA(s+jlAO=IJaPqn@RP{n?aAwM>pIzhx3i{ zD-inIpEL~&43U+SjFq1ktFqlY*(Ndh*yWD6-sNS7UeQ8jF#C5jp}m_}zoPR#mM2I= z7w<85wl`OMVm#kFULvQYJF?*Uu&k-v$6{p_t)1y*2K|L>3tzm)Y_CktZZ|X7#MLCC z#gLIwlDDdBSho!>HB+waoyDXhF4Xw0J?2x1j;jsWRu2pejH;7zY>H;>d9HPmQu$8W zGH`j>fxBDhPlN{c>Rr3`DBrx!Y_Hh+RrOk*v9R5T=HS?4E>@Hq*@$);#p`+QFCee$>oc}ViM-UM z%+Af#i0s316rapp~|;S^C}Rv@B8|JYWXNA9C0WEhjUz^=P2ab@h*n` zr!;bLg)ywL$l5)8qwdCh)l$oIf@8K>jc?CF2x-sa*AmAeM#22r`awQ}78#6LEcNh_ z?Cng2$4O~=yDE>p!$!_$n(b~){m{=Z;wEj>1q)R#m?7QAH{7;8H#3|EbF|Z> z3Dkv41;#vKg;*h`3A>xlK8+{1p^g=wKWEe*MpmC@G1EZ~YS+qaKw9hxyw+)shO)QE zLsLh@V#*sCh#yN5uXW3*Z~Tz-pLu9xN~r&o zgGA1XyJi}Fmhs&vI(6>+qX*ntXKQXxFgu1J9l|jE%~kVeiBq|yaf?jU3B7}5*zX)D z%HX}vkGgR$MI9ft1sFPBXrZKobLfG6+_=(14Pm61Y5m&zPGfR+4cR>QD6s|nq@=u0 zTpZ-#s*23fMkHHJPpM5j*W$lU5tzqS<<%C>z&Dwuhsm~32|vmj5T81XvHeD72?g~y z_z~c5&%?7Rpiaj&ymhnkO^R=L=;^<8Sju8bnN2*-)K)t#+jEX{*dLoaLVYh{AF}=9 zt5A^U%sUjGMQ(MjyC>iq$Npi(7$U9vz_p)0lp_Tlzfkm@5MS)ja^0OS`cPkq-;akA zqyC(JUQ6QWQu`Gf?xNxhv5(I(aoLy4TzKOo%>1u-2JBTHt(kI=+JC%6E_2S8=-V8K zV2g`GP_>>B9UqzagrAi42)=df8fT7HpkGOlo`E6FqWsMtvNn@b$&r=bP(jz3deVV1 zpUCRH9m--~n{Jwnbe#Two*4gommCpJwO>2j9Mq9FNy3imwa6eHd9+T$;9#WdWWTu0_&Yp(b zX+iGga@@q!M?clkG9(1#caR%6WUd_rOHdh0|ruda&e^pAhhq!_Ie6K9~%?3c+I>gNsSWR*T%A? zqkS@jkKOTlZ^7yhYv0d9bGeBv&-?XL3e5>vNNzc1lmC^2G^1GragR5^9(AS6BH$0m zM|?LK=nci<9BvYjh#us7H-U=Q*7ip>{D>#<`fS~_t^!Z|b}N~zdgll|yn;I64{Fsv zWxOp!=+Q1OeV7d24diljC>lDg8eER zpGCiikfs2|71v9tJaZLYAAzj3hG1MsyLK_d0?g-lv{N4jynDx8B<}L5;j6QA*9*Ep zA^I`10wKP0y$T#tz>uCWJ()dyHNO62o4F`jtQ_S?Z`dilWYW#ojvAe)l{LTO8ENAF zS`RUdhd16Ju8Awsq(n0xp+#64-gq8b{}LL_Ds_f0afa#8h4>KC-S&jw1l!cps;#Y> zH)Z`_Xs%L|;@`A#?h&`SCuS24Yfu2GLZk3%s) zF4DJmDk_yom`wD@#18390&dTp(S`rb?HJ?6*JtfEGWF3qazcZoTl-_%Q%&1-TdmN( zEh-jZ94xndKJwJpm@HtVL{vD`!7k6~MZP+7l@h()SK|V~$cYxEdj-l%1@3rnk{cu$ zX|?YWwx~|_J!LXuIZV1Z`HQ`A9su*rwUX!{qsI)lzq)f5m9=zApi74b(k6de$Kt+2 zA@FRw7!%ibuP0!xf(Fs#pwgEDAiEQT#U)tw)v<3`N|U3mg!%E)HZ+df82zXF1sHqP zR_c`c#*b#7GNMLTkB-{Fe)_J9n1{K)Z|eYBCq@Y*{j8yWa0-CM2stw&2l zua$XCsl}yRk;U37yT~BPKcUDsD2H7@S#3|0AH}6 zhfHumk+^+=GX%4jdKl7j#8t50D#shZhF>Mj1W!5`{N`G3Cy=!-bQ=mF3H6ah!(RRQ#o|($4#01lI1w`QSdFZtrjau6u zMRN{$f%zB=hQ$Qf%k%aW&Tu#uT9NoGBZYkcWgGt9cIsFt%BrideLAdWR#Yn;+b;jH9D-3j9rQh|;GpBSaeniOUY>DVJs zlbc$9d-14;yRK%2FpPiW>$;C#rYr!Nre)I6UMfW(n?qUJ8L*E)<4Oys-b=oL9R9H= zwrwDWo14osb0uUOiJa%-kAFNaJB2Y?qbA)#UZVY`TOT0UAH`=DpVe=|&txj4-g8po zhdc9X7zv2O9Jt1c3Qc}9h|GOCF$=x*R}O}Rt~{DuZ_lO0Tiua`t05Q=e#gab+t8mN za-DQt=qmIKF|oB8VD^Y3-%_t(q1kT@svQU=5M8kI%s*T(hNW%!%?%34b+;Ov)*wQn z(yX=sby5`g@$Wz(8*>xhpFIO9rNHVqeLBaAXK0o%58@{VJug`E-)<#E6szNC8uM5LHy>*w1&2J^og=PzXZct8WqNPnEYv%6%~S}VsXb6x;gS;mUQ7Hnx(%y6#q+$pkI(Bqe{y=B z-EMJT+lCF4UgEr|A-|G5F%hbJ!So24Q<67TL|uj?XFt5`y#fkkc^^`x+_N(?;fw+% zcIhQ{^Qu#|7#72|z5GkGFbrH_KqyJvIa#XK-Q9gM8eB)65V3ip!?;Q#y8BFvy@TDw zkBBYm`ORyGcD9qn9J5{aiUmn)Buq@lJNr_O!>0|i9X4uk34Nx7*tDU-Vl@#-Sl z&>e8$v}LQM17k*i@&~s`@psPgLQ17N(xlv~RdYT)p#PgJ4;I%KcPG|cYtlPy6y)V~ z?`CPx8^9q>m|NN~A49*5pl$(p_Dg|~e8Km-Fs#_~i7+|vIN(sO%t3~4^_;o7ei7Ur zOpfOi&z2H|y}ww4pULZW;L55Zs4niSfH>KEU}Wzv2;h#?A|7y@Ln|GoZZ2y#{<1c0 z{t%lY-csaPkPul=edfjIE5bpdRtU^PYZlme4#f9E6qIMsy z78~Ops+>6SJ>`;!VrDYr4^;T*rx)U`h+nOo3M5EKSk{TX z0D!iEdWOYYtLFG;@0X6yKxvB%KxG5DkW|?O!DIwBI#JjSR>o4~gImDnSDdQZ*%gM= zabII?_}#epBn&`Oeu<5uos(7;P0Xfhsytf&WK41v136v46N6-p>EMi3I2~s&TpgVK zd5dg1=$-+5eR6g#?@-^0;AZ;3+dRI@Td#5p3LIS9T^{jirAS0j5d(wdUb}?NyS24x zvxiz30}V{|rOKH+KCZepsfyEo7nW-1iK!OAIOh8*0BfM;39&r^+nP7Au?gG*l~O$w z_PR@l6xv`7n(*>oC&O3QM~q6gYP@)EuSWz|!Y)2%w4P=gc#J_%~qMowzV`|RU z)IzpmA5Pr!+=uV3$3>%pSpn1`L`#ckKzRY~a5Le!s)Q!@Ua7oVVh zUqp~KCTYD@i37*F@|*{k3sWxe z$=a2XeG`k#65zZONn9=J_U$AVkShSGoa1mkLc06WT(AOB^>qNCgw;0ERbCsALm42z zdhfqFT>vRCoEWn?KQZft$3u%XnNtO8F;Et!3E5nG1&U_G#P8c<9+c_$3=e&xbH;cRA9dEYr?&Wrgm0H#Tai{1POg) z7E!Vm*ODHW0$1y((b+Hz$q2v=I351@y@dxrEOu5oY~OoS#k`)rn@8Hv=ftMqjri zs5J0xxGU%v2qgl@(8#7PI$AnKz6dk{;+PT80qqmt+_VY%H}ePUHH$Z2EQtcPyrKF0 zRvI`|{9_Rd22eb?x+!6@hWEZZ@HV+OFMiX@G)n5a=SOH?e(I@uAX*!uXl=-?rTc)& zVb6=vqoAHkA1xNCi$HLg1R101KEMIP@22d1BZA-OU+4mJo)hOh53@$RBH~1RwWm%& zKJJFi?zsMDqEtx^C=(ufHnU6f{>XfRVC_YaVMQ~$?3v074;fUevWPmIBQ{VxdvLVs zJ7|4igr&}z6-cZu2!ez@K4T;xk(>qeE_?$~>#aGbilGuZ*JfXV4Ua0mv@^akHJC4s zHM4|ZMkwH9j)`(L?c@dke;XL4h_(T<4C){w1y=TIm1K|2g)_*#{oT> z>xFaV2KM%1OAigs1jEmgr}|aa{#X?rz|D!x4n3;^*;E4kVBQtsp;`~GF>#X-T`az~!gS>N>3pqBObZ2G2Jgcx#AIJtuX z{uk%tz6xC8Ep&rlrOU{-GXLehZQ$SOE2sLd2)leb9*KG)4fzagJp38;gEQ|Sc&3Fr z5+kD6xhuH=i2}mk%W0Z@cDK}LYXi88`mSh*fV{E7zW4aLwmYx_Wm2kwARnu;oy|uB zHltN8tX--dvskNvQMs7$IA#&ZD`3uzd8wKziuDP&XYRG>sO*35R`?dL+dne($ z$YR2h*~PB>in&@bU{%5OL_~h4M+6kfOgFb?x?|x0^;)&=Z_`wcsmeo-kU}A#uzTpP zPX7pOXHci9tv6;@)n~$&hs0D5Yg}HCI#Qc9wDED0B{|#gl&l(_ z%%3gPZ9-D55xv*LgW@*&>Ny=nHz#*@f6ji7I2=g*yc`1R}88}A>9-#UBs`t>tmVPVVf zi>-x0kJ#p(PT+;Qjo~{&$)|M{NYF1}fS_uQq+sYX>c9tQzI7pX%IjLvo)tV!pGL(e z)ooHK5MuJ0vS|XICGTL&Tf)~z;{7rZXDhMBO;$FVz(U?!sQ2NkCVZ}@9r=`f%L*OQ zm$7z|!@z(4u-kN+*YjI)IE$?Mn)MOBARo`9ozH7xj2-FcIfH{oA*tm%jD>+gsZ4oD z#xMgY3u5hAd}9Q4o2C1-%Io_=;f7;dCtV&Ezdu@bB(y^1C>2@~FneE{K}zs)u6|3j zXi-;QNm**q9J(g?YEa%g_}Q+VQ?ERzBaD-!^TK{5-LkJqeb&u03aeCza{a zBvtnJdd2yn^S#-dpPvb@pbtZmB-0vo#>!e^CnsvmTNYwU+}P{pggK!hOk>py{#1D~ zkPb=$1LgV(86_6aGn2Iql=TqRk#gE+kq_$g^e|s+>3LdLi4d}4GEF-C9DPyv6lqmV7le_0;9*A_tC3VX2r}_P$-Q~VOGz54a zR1%q^-c#o(DY#*~psE9=1)}EKKos10!Wm$AHggmq;y20XhA>OVxr}x^O%l-q4SR^- zm4yB`Be+hV*C+cq5L`H70-sJXlhO-1%$)R%I%kL5N&@Yefja}~flA3#D$`S=^7|@U zc2PR~N%!t<2D(yVw@M=!0w&L>5!uiJC1bI8${#48XKOmvOQ6K<(4#*F=}3vC&fdN$ zuuNIaag+qj3}E#$Bh3!V?);N{*zYe-ioCoWEWtxf1$a^iO@#6J_CF^#pi5c}Ke5x}L1YIORXPDWYgh{>TWx$|*uM?up-CkT%juXR7LW{D|3JZ%>q z-$#2KeL7n^SR(g>QO_}O<8cAh1*Yg;{|BZP^{_-(2Cpcd%G+3%0vUm>hRrx$2Xo7{ zjgOiFN~@Mb!iRnNVYQH75fjvBwtm@e4e{hh00ZFJz(rcAp4cKH+SX277-RP#v~T0X zgtbGPxZZ&M9f0H&D% z8P+^rD>Ln&9NF3$01YkRVl;6YQZ419t$7$`2w+B(%`8y)5*K|eZp6J)Rv5UU*eMfm z)lA7G5C`r|agsCT+N1m7W&i9}i@ne7NH~PlE*+nE2kueA;Jixnps(ls_V)5*o6a1uk>^TH ze^n>lHNNESRB`vI_si~S1}kILWAXX>3yq#RdO=xhpQ#_OOm`-NMkt=O@Q0e`SGA@O zN($MRij+60&YzGeOn;jaYfy}f1e*`lmsMR`8XX;zraVn{m|7HFmZuumNf)XgnCQTJ zQ_l`nc!ZIODFdfCv&ZGTy-?oXnkC10!H|Oi+lAj(@5>ks+TUGnlHYsf|L&FaLZv}i zf4&iCM!=7x2PZ@tSLKl-m+bosOgGgrdnqM&%zmAm`RKRQN+7MIc=PqnV>HyPgCLb} z-5;=5+E=(!^8@S7BJT7=)V`+yy5FEjYHK~2;aWdXpxpAa=6J)Pe!gq2 z`Y;m%L%d)`#b{xg-bc{(W#Aj#r8BV_U^4gw^vy|`6WjB2PMG^lG7#$&px^KLPHgji zIWCVkB`eahsvb)tHE-Nwq6Y$~F*8G%8E?4K>DPMS$^~pSul1Xo2NuZaT`hE}Nu72# zd-2j_!_1Z_4`BASHLYEFSslBPd39gzvF|%ROu1+gVz0`+7}G)@q`>Fc>EqsAHcaXY zn)Vo6Xu4vqdw%_Bp&uz`GU~t5KGm+q{!BF{f!O;tCFl`Ae>;78rTRE^lR#RM?gZpH0X5l-NW(E-H8!Vio}!HT2j0oXyPADWW4!2PdIyFm!S&Xbo#V@ z&`z{C`f{#At`C$}bmqT(eygR{Teo#={T6q)<}NuZeV#hOWeoHe-T&nNX6)lD%Qks7 zw@-yLRI|0)i?M^qI@8qYs?jmuA76d0Gi!OW=WqLL^;c~R?#y)(|g5M(ItA&b~W~p`Tp>9(S1eCiu81>xGmqE-stqm z)oa~iOu5oc#}F9zFW-lj^;Y_IPRY_<3*X%TLBX|6Il1>DN1PA4Xznmg zHTgWnMHrK`D66FxrXP^LM?N1|OYb6v(=Y2u@&2wquI$jEs=dWjD3|JkS8^OrfY`QO z9#?l%UeTN+cgU62r@-bO8=nTor;EOkUViM(&GFou(kix934YZb;~Lf(je0m)j@wyp zHOSr7ynEYN&cgj}zznQezPIk$=qE z4yBk^8<9SzD;MCRH%OMMoYLU_z%T4Zd7vH-PTKf$9)3HzDq#55lpK$qr}kOgr(GGn z;Q?cMM4AM2dXW^LOQ3kwamYO&x}=jQ>03PWh(k}hpYq4u#4?wJ!kGjEy#;%$i(Pwj z^F_bxR3FS0Wj_Qsk=o5m-|ERG{p`~MDX@gywTVrD=D0`;l^pf_5Mmebg?g?Lvg>TU z_X|{CEU07XAE$+Jte<&kCi&HgulIY^I*!{N@S-v+!LjiQ!GK7Mv0P>PxcewP@B{X> zrh%a&3nDEtIhAiI*10pF&_MQ7D&J;WjB%ddI*ILMMX@Y)anEA}wDCxIsks(P&YxQ4 zJhy9ZZ~t;YU~F$gbxAu@e{N>a5uevJ*S}z7Xiv4L6-LKFYXJDe;48^qq#Fzwmkjk= zS9H>Kyes!*jo7Y{(c{BP7c{uXmr!LE2YE5x*G6f3EQV$-aJjf^5N(I zyE8V?-a=!&1+Uj8hMD<}e*~zzFr@aWL!f0U9;9)6M##a*>M?9#29_SN`{B7@C%L!# zGO<;>NFSi!_8xb$yEemy_E9>+Pp)u*7;V$TrNA0%{JN7J7!f?8I{Q3a8FTzDqz=xG$7o73?)ffhby zMPxVz_8*uU9N#B-iY%3B{OY^9PIMPb211yAvG*Vr7xm1YE{M>gCx+xo5RyB`Jf^XL z-I1*xX^1%S%Eu^xxTdl4Gwz@F1EKN6t|i)U;3E2eZsz|TxI;-$pYO^)wdp@7-qO(# z9m6d`aU(N3`*Mfmisoj!h(^04vB!VLd*&16BHQDA@2{_{8e83o+1A$Wi7zW;+mp*Ffi~&A=Qx!*W``+-h|oTV2<=;E z?JW$iT7;HFI;!?({;{%G{lZ@mV|L#67kU=hinRoWK`Tw# zVVuR+Qho@S;f`2d3yh2gD-qrhchR*KJQUCO~Mg_lpDOo0dPu*%vQX zPFn*Qwft5?Fg_#%0&!xFc@o@|jZ`CMXsaTf&BTkE(RGzLB*jH^sf($7;3s>~B&Af( znd1j)K_%d9zTHS%QS^5Byr!l|hd@1MuiZ>=8Z1^G@ttKj&wC+G@V;F9otzxA&Ws*D4_wM-Z*Jgcp8R9^bwFThw^EtNZFxL;SW3GjAJexQnP?iCBSW=4I#zgmy44C?v~uYVgF4Bv+KRFBd4@ G4Eb+Cf0G6P literal 0 HcmV?d00001 diff --git a/docs/assets/reselect-memoization.png b/docs/assets/reselect-memoization.png new file mode 100644 index 0000000000000000000000000000000000000000..f31afb834bd96c6522db3129b4d3dc6bfe125da6 GIT binary patch literal 35089 zcmeFZby$?&w>L}+A_5krga`;ocOx-$mkcG+3?bpoxN=xoNRDd1msWOu?Q%F z_tp-UcJ7wWEd0C<)+_=FEd2cT5Jw+`oVFNDPt?c3NLkC$%2*p*1|wW7oo!CXXj|IZ zaPeWUo=oEGt>kX$X0PFDZQ}&SS$m(35)lJ;VQ;kdIlaIqEG&9@#m@b&$DPc=&x^gF z=iqxbKOgVeFoc7(jmO!|ru3rB<%+}TEY>{VA zbpz}8r<<@B^sFqM{_RyG2WzW4=k-8jmsHr{|`3^of770>4Z32`{@M_xX)kAz^=#bY?zHZ+~#bT&z^O*1(px~8h-jf zUN_5sq2XlcUlL>E>TCmd_W^7AOI*(GI1?5wzQ3fv>t9$eCU7>${%>p-6g<0VdA8kl z|Ck#{-x-g<8)xqmCjR>jJrmWxG2pJd4Is|a-45Xlxaa}idtem+T*AJ0vh;w1e{ct^ zPC)(*v%Ksb;5K@0mR4A9dI9ZV5s3yFvb@&5I9wwr%vuaGD=uYm9=S^pId z3!KT$zlfNw4IJU_0=VdI(HqOj;cOC+C=*uTp{er`UoQ_}y=w!d!vudwV4 zE&nEz#sCgaF!gVWX=4pQ>r78wTwQ>~{aw_6?7I8h1CquIuAh7`{`&_7toHrmD+R2u zU_bqDilm1t!u>?V0a0ht1qT{qa|SwRTaG0V%nQc+2MFM9<75eU@cjFr6E!=z=Rc#( zf~QRS8{>2lSV41eaYJC$*2BgL*b7&843)mMDua~zW@Vgs`9t3(RBUK ziZm84|C>enj9vfzB7MgCzwzyVIDDQOA(4M&h5j9*&Ie4BF#p-OzYCn;8MFT@5X$#& z&CdT|3_V-$-!b%T-~amsPUJtp&{IqV$Y+aiv4T6ey8KVG+rUhViT!^rcV}DqH+}t| zi*x^u*q!bE|8cPce)+$IIVVo~-(txV2NzuYD@y(A^FJb3`=Zw#I5>&0wE*~H${Hy!x+>zD7D!vcm` z+t*7hM@)S7e>;4W#la<@_}dHPbG#kmY@NZp+y8VK2bWovdiL($24e3EtHZGrT>5z> z3vn4I?MKxZayP8#9y9em01A+C9=G*&G|{PaQ(pOsS+wI{e-~=~@w?Ygzssy` zL-J3RmxocgmhYcZ-g#*vp_oxnpxn{X@v|{kZFV;3NRPX&A>BV{U#fpLD^0WT<_=aZ z9J57AuY_>Z_TzRGJy>&@*m zZ>=8HJkrBz>iT&KNaJ{QYn5{N^DyG^ZXc8Bld0z7> zwS-Gb4qQUZCHYsdUa@i`7>Nu6@VWt zm}!;g{a}Xo_UphgdbHpptjV7NgzN$TszWz|wx!}?^0@04$BFYU8K0Tb#$pc0D(MQO|-kNO;~A(g%vwFxWM|0c(8U(cX2!nM3Xj%k3c~!rNbz5o-*~OEHy$|sV0ZkkXnO5TtOVZ z&e~pB$XzHlqyUz)#4SsWs%P>##nueDVBU`R6wg~|EGtcnR9uEQ;ycBtz?^PqFz3*u z((DP#Q-HJ~pwXiB(uUNH)@}C}3z)M*bnLvS(enR>aKQ6}%M{Q9lI%>Mhr)m*YJerE zqa2|VmhfF;jQCW8Z)zxMIcReOf}oUTEWr!ud*kjdaI#Cyq!bX$5kbgsV5_~oc}gS> z-qa<=2;X8E2QsVz$wm-9BFi2f9p(4gv`&->YJ#s%Nqs^`IsY=Lc6oWQ-fWTPyTq=d zqVmkyCd*XZQm|v@N+=Fq6qrN2NTT2Zc8=h2!bc?8xw*OS(*b*ld3m|H?Z1Ehn4DS> zA4=yPQ~KIcpczd;#T*>0Mo65^y((G+hyY@)j=bMm@U*PfW4>drX-~>$yHAM|LLii* znJlbvo|LpJ%)!v`)%N1R=flwyF%R{}J3ae*>&K{1^Yin`+V$SQ9&Dk{G5Bk*HSU@B zmFH=uIv$mnH~Mb&tMRnI;xe&r3C6uTzdBiOWZRX*A|@ur?>Z&9HU046!?}so@#@Z0 zuPKM641~{IqTMYK5hHW|Uy57k#ew|JH}h;0{3P(`N|-wh=~iZq<}uE<1sU)pG1~+DR-Z0d+4?w$K}R0`_>|~5 zwr*y2VeTv|_JH%06v=80+y;Bxs*5|@W5cKOK)ztyG&}|dsy|O;apU*C{rSZKq9_O{` zxZ)A1`LIe7e_1#2apNICnW2zCDZw|zMvjg}h3WeR300qq@`>QfZ>GnK)er}baLnf|`=>9&PVw&AIw&YOSq)N$yg?!90jFtu+YIZ=YMdp+$;q~g<`_OdC zpC!BY0YX2Gkvwnc4W8)u{jTuRxlfHMIwXpp=ti;9`LpHDGW+q%mW);0O~2Y*-XgsKbkt>5 zb>0Lcp^rmr-k)!H>8o>H{P<4V*MU5Hc-x}*=#|Y3L~qOmCjK@JRu+nZ8EBw}VZe<$ z=2S(3J!iyiEqE;cxah=3tFg$)<8@5`=SPj!QbMV+xe*AtDfH4qYU*8u9WuYtvgtE0H ze}yDgmC%>a%q9~6J0?C_qf7f*FqG|#ge0o4L>!~9ba}@VcFqjuTc~+sIOutJxcvESgQ4oWn7-K9*yoeB zhy+X2V>F$s!2o=96|EIbHk?@mw(;SrUT|?z7R)t2ZL0;JyseX`*~t?!{JyDzZvg)O zH&uiQQ*dxT0daQPk|ynAthQ3XkV$!qpXvgUvqr+sUmXzJ?g6j!mS?AOoxu{~a4aiX znVO=oBZFm$@ifV^k#x65m9EO$v*tfl3Ff6`RdxaMlwM(ss4E{Y5X6oP)+0=zhomme zjrdYRFk@Slbhrfiz>;{J`tP#T068r`Y`;ShXG$EFtYmw$8j)=kOKj$DRh zXR6ZxN!ACFtmpUCmkImG8r@*tBzH*j=zDun2|*bdi#+wjoBJ0H0z?Xk`o4Vm@`iBI zZ5UnWM+wPh(GUZ7D}cK%`#ljqz1y&b)N)Yzd%=yEmoIG~>L|)VF^{>|M?L_-Y<_Qi z;}1OaHg53#RSjViV$^)ioB4f0GgUk>rZ1Es@ZMV8GB<$mXvwWF0AJXU_BsQ0)MWVC%i?L+WDPOfKL-TZ?6p;3Bfv? z3h1bfEeFrP1ZbFw=OhCsFi_Iiaa-OtEtU$=u?VWq_(l91AjQ&w5)RIZF1nqsk%}mK zLZk_^8!Pf`L5IpV<_QnLACqM7YCPvy&UahyrR%n1dJT5)IONPs~b&aT9U4YG`Rz}0)bSzqebTCiLfah~}vT7DV=Pyxya zFoH;qk}=|G*2h|)YC=HOMttLUPrmyA0ESjCaOA2cHNq@G$CBVL9GN(O3cw{IM@C0m zAMUi9S6#S3;CO{v_(4d@WPbHFowT~RCvbvM&>?R!Htb~P;2FNTY!2s6z>Jwd+AN)O z!1X$tg!I7|%AL$h-PaZpU$Ks;A7;YWJ@_mKN6i#h0r_}zC5R;3kmx;+n<(t>W{J`MMaF9J*|0MFI_D9UiU zPFezZ(VBCD8MmM3x-C3DKUWvo->>U1UYe@zKO?>9uA_AsvNQcmMI{UiQB7c}`+3C< zw@;ofNI2JZ__)MmJF2AZON8y)_}NRy;<6ju4^bwa8w^J+gEhb>jTO#l9(3((Vd2V1 z!WxB1faVC1pBGQ}NS|Y419i;eBjMFvT-I>YdAv%I1ecwIBbHBO^46sh{ZE<)wUm80 zMa)_AJ9WfNfLAR>ctA}NB9TBIfG3$J>+tZcWWc{Qgq2`Y@w! z#br&g!p>s~4cE^To6oc$@i$je#5Nb0quYe08;whA5po|t?ktZaZ1wx{DT;V&b_3TS z8pw@Qe~jRXA#O>;^OKq8rYkpU3w3Q`5$hDlwR}v}pXY4iFj%0Yz+tMZdowirdCrcp z-Lt3Hqk0KJG#FD}&adLqCr2ynM7>YuWE7w9QpManZGpB=8eiaNEC};1PSzWw^c6g` zQc+bA`_Z$c&jd$GBK@0 zj-nC8{!PEE942y}!D|<8I*b=ueeK-XC+k#Gv-zCYRsJJR>sGA+r`TRAX|BfSi)a^5 z9rLux)j^f+HUG8hwZ1nx%+FkhBNl7X!?SWAJgW!#zAB}BioT{y8O%B7c31e8@wL)K zDTl=wI631cN}RT|=IN!rKt$P{$J>Apy(C#qHT=A8!B#@V^;NV{fW5TnLf;<$qK}V^ z132=>ly(zTd8@rFmM4bimk326TjZ zy_1JUL_n=7=Kmzr?eq9PmNY31#ZwFN&Gf z4=5#Ne>VcnLTM|;0FUSNt9je%g$m>5?3`Gx7u0~ip9*C zWbG{lO%g8(c)zH9sY$}!A!7}(jMIf)@^!>5$sP$MzuFe_2ye93CcI!L=8;_ON>Xvo zx{#4tTusf3HQ$DY3H7e1@RseB7aXnaSTE)=@HH+jT_`%k#W#3~Wz9^TRCffIDbGdG z+YH}W8q~Uh;QG>;@F8a|HYJH8GFg~x_f?9~vzEU1p&GX*k3UkCv#==*bD3~U9qhz$ zLQFZ$xR;*MoaovOFgv2PI`?oA_8k=791$*`~!31@`kw%RY)nj%kK zHgy8HkvUnWh0}pPQ6xP(O*SNHqJ))5oL2 z&jn~te8MZ0evxJQ#^RZYSBn>G#Z`;+ysGC);>b7+l?Q4>1yqY9*4$5Qi#`uXH>d@xHXSMAFHWtF+YE#< zw6igwgqd?NW%Y*LOTF(bs4K4l0VW0}mlp3@{!PG2z*{oh057l|K(}7q%~n#W4)^OU z)XV>353c>TeP&xdp*@YDiL5(7M{M@%iuo_RxGwIdEp1k9E^J-)Vvft?wNAg<(+j(t zq^l4>X3`k3Bn^KT)+$P5NgsAi!=w%FGKSAv%B=KEsw^XqT&*GB9jEap}F|hvbDoy0`|8@ z%7Vr88SAT^=HvY(isi-(MacN~0eE#=zrG#%w~z0=bTVt!bQh?|2}t0! zjjyz8Zd!v;2w^$edK>sm%&gUNCqlq@5Bb<{FKH)}p>eA;F}7IG1984I@q1zdTdtBC z*t8}YeXs6Wx3XXt>0G3~*TiJ4O|k#xx6tzA@JaWNVc+_X>nLMir>snU((wSx&$tLI zxcJON_EXJ5Q+};-X-s^TTlk%6{)H}`*}S_BG@<;$%GSpQfLh3dNT_C&-w-UgbyeZSb@5+F^x z7Hk;XSv-52__~z;*M@S%{JC8k$u{*)=YhA`*Liq=JVal)vN`tZ_V#1F=e=gJLNDmQ zRwxOV8CwmwtM*W0NPjiUtx4}rZH z5Z*y^(JIHm1MF4Ln@V5mB`dkeAZ{(UxEn3=Dw5ME(N0cDd51w5wb}PxR*!O{5SYLG z8(I}7?rs+I8z#ReqkKmAcApAjVx{3>?Dyouh;#xH3QEZ%U5D?8U(LGmndA!9)kLF~ zmFb-)PUiKHVXjWWvb{c8-?b-HqNPpx#8{P00Dh}!?8J$)ii4XRT#Ap*oAq269&*wj zto{5X3r0kGkzR0}Tti)xzs$K$NB3e7>2(>;k_YkITV!^xjT%kpYbSBx5} zp1h#!OmjCMt;=@r%f$QR2%xYMs-%*j22${^wS6^5wo;5N9kI#xL4YyPbCTRAj+HE< z_DlsCU02vbf`nI6UvEUrNU|QBBM%sYF!nO%Z?%P*vib1ksCsdP*K!@A{NYDcfm9QG z2LN#`*hIG^HeIf6)K77uulGnx0@nio`tMw)Rq)$gh85*xXVNMpe-IKXbAG0#9ELDp zpC-gpI)DDl9K_FS_7fXH)IcB*i)zSSv|G3v+GpjX}rZg z-EgdTT;eg`4MOg5lyBQm0A>)HVG69)7iX;@oA( z^jPxLkn6TA^RmduO#2s8$D#>U3|8OI}ZP7kpw zcnjR#PpWC|C)#(N^ZpYZ)J$)tybVZK*b;QmLw1^XdSyly8}^UI#b4?j#|y&xTHiWn zKbWHEwbM_R2oOfiR|Xv)*+sl?#7X71c|H+zj420s@8>akjV@CSKJJ|*pL11AQ$%F# z2~RULOC2uR{U_Xux$-7dhb+0mwkwvB8PdjC*6;uV`5ZxGH9SA*yNt@hZ8=g4y1U06 zQ$o83X(7Is>@+HKrB9|h`Z-izTs$c)jr+%^$he6KQvr^m9f)K}W`TAMKbb-e%Wx#lFMEI;j5gzxsB3}fONMd0ncO3O++S-j5Wf;$mV zx|W4OQw(8q5n06ReQLDJCgLv0G%FT5;WHojP{m^Yss-38nty|gDPv%ujSjW~79(o$ zJjZGN{tE6g6=rNOlov`TR7zry!QE3}aIMF&U@0XU_$%y z!1Qp@iki@3}C=e<=jZll={7{8sT(^JJ;qSMN1T z>b>IeH`7d)=E7Hsj1NCYCdkF!(UXwX;O{V)^Xyh`W&Cg3F4Hl&qR8O3Usj*L2n2{8 zzwoaMY;?N!R&{Blj4dI$W;jF_>*X+xfi1QV*Q3YkAmD~(ny4j2UVy5JX@0k+m!MCR zDuri%{)vwdYnU)B<_}%b0uD*SSEI-vqGJ^@p~O*f9Y1O zx&2LNfE@{7*UkEM^et9sisG1fji5eQRhLYdl&xFG?qX6-`NuNF^Yx|B48GWqryaM(l5F5qk z<~y)q$(Mc7QiWLVs+_>OD1E(0)3x3HDB#{lJY9mDk!3w=Z!ch)alj?Z!3IQWJu(Sh z#UpKb(S|`%s^Sl7Qid`g=Z|at0Hp2$o_yWI6~Y=OimFM8ZX;>QlKe(wlj}PMt<(&W5DItHg`#pB)$&5t<6bR=6 z%`WFlRBX8)KiUfF;};V3<-}jvrr1CS8w;eS^Mp2y@r)0bB}{yAau{k#CCPQb-#-|8 z`UzxLegix3O=LFeMC>kJ`dafKH}O?5UDbu_QmXx-7x~ZwXm?r#z?G5E&9Q6e5VQu* z3WF1?j=DJSH+`+rkp8p5JPOYA`;WzMurv;Tp`XFBvy}-bL+H)D6V|$=1Ru$z)@~pw ztXvMZ>R#_;ruyx^&|}j|!g}qOBFOPUaWYo9R8m*gD(KExW$%>jh4G!Znmh zxXWPH-O8WJ4?n_8fHIJS5Ko`UZE2AGvhvPVUXTNbiIzFkzPL6D^Bf=2?uZ(Y6bCLK z6^H@Nwrj8vjBLw8yt>8bnw^u?0S6*?;W>rbJ7d={v(WrmVSwll5&3e&q6V@fM$3bf zKor|wp4+|*!Xp;NMr<1wvwTlD{O-iDsL;_dm3uUrQ!qb^ z`SMYmwrHK)`O?T>LzQB4HwbJqNm3*qww9oWb^33lJH_SoI=7iK(SSIJ z?&?Pt*5@uwfT<0YB{#!b!Li0SO3{ahzegn2=s8~Wh`7W@8VSE$0JtIZcz_C1`cY)gz4y;2e~QIhz^2& z&Qdz2ou#4h`->B_xC9e-z*)b?x{eoC07CKVXmsXUML=%C!V}~~*bAMeV$z>m%1Z@aVkGtX!7fdpP0pFG^qB3>8>Ic zh7I5%TFaPB6?U$!1NqT+adyPG^TZ2-KA1NJGxw)PvU5f`S(plIjg^w5;mV4NWe%gz zr&FSafqNr%sd_$+z((tUCqkYi%ALdva+JLmnh5LJ2J=SipV2OHLTb`4?oO+Bf+(ca z+gZ(6rI3bfh_Qo}J(Jjj8?W!OD99eruuOtDt9&s#+IoC{qD(}Dmd_N5hOx;fqj_OX zRQ_)#OOEHjz8nMFg&7#ByaM#y?MX{>dZ1z2QP+A-NzMoL_8a`WL^Qu1uXoOlTolUvLXA@LF$k53MSB07AA3Wx)o@h_Fd5AF?m0Bhjz&NPflg zZTi-(KMDGs_^gU~MgoZIdR+<;*3Ig^R#WQiC$>ElsMr4cgMyeLunA#-$6n1b!>i+o zjI`5tVn(V&>*)$_;7wl=4cM%51xksOL%(FOj?0#7RF&Gab-3c}< z+WwI^I{YHj*4o_l5*(r>LIhn$<+>X;qz_@tG%_D+ev;mgJt6h%zY1|`S;$1{sg5A zwx|M3n40p7bH_Vb7ix|qbc)D60-JsZ=*Qc^d?$kIw>N%u7?j&MN3Bf1l~;Wp{k+ZL z+;A*M?cI#DK>-Q7w8`A?lpu#Xa5C{Qa_H#e8_NFTUn+Kg!Ub4coLm@Mq>Gu_mWR-pPKB2vW_;oFa~Nd#@=X6&WWI+xTd*F|pw+>KdH_rKYktcQV_d%8^B zWZ(`|k~T|bwpbmr5tjVnmJnP@sp&kTDW1O*yA8^#dIAF`hj1L}0La}U6KYe8Ga=fTzvl$(9hsxYL(~nNR>ju{w-yO$9%u2=PZnA4f{O2Ra_dy#0N+Ss! z;5>YbpnTe2ZF_#Wl*?pRV){?ipN&co+Qqh@^yN>BUw07Gc*2? z>{Yz+YS)A`Dl=l&q4U3pA6qMQEIF)zup0FKk(xk?>B zYB-pfwz09z6W~zb%Hnn#k7iX>k!v{o-lN_VB_0@m{#FEpBRjCOrlbAk&6e!w4xY5Mt}&hTZLuze3~{8fUS3CTs(6J1 zW%RR#gSGn2@aZE%dhc0_{-Xx&FmH7r4sS2!iV^FxxHUAGJ{5*BeU83?PlEfIWM!(+ zWIrdrGa{%bVT)&xS>wl#8%9bTAkox!`L~X*MdHD>K!3IC^ukqDm8dJX!5LG?2Zcn; z+D!nc=N$%X-ZC$%T~BseN+)8grQ(OBa z`AT_tcQw~JKWfTVkPBo;I0#EsG+&xYcq)fq8=WEYn!UtQ|{D+5UnOKnb;r{3{89`I!#0SENi-JTA5DBhx>b#N@D zW^xw|e3I)RuyVWYJ5O&&z3Lt4Rm+IE1PsM|>Zd;qEFn7SGB|?MGT`yY+h`LL$0s_I zb?I77lVjn7bjS@X{{C2aS%1&6a)5ANt$NC#0q+av4bGcq)Mki-R8mjq000clb*80I z*=-Ot_f>extALt{ZQa!+<-gbuOyP3f7q6?VXQ$7nB{(bKx$Dy8;ANMKOMd18w*D>T zKQUWG#O4J8Q0qYUy!|B`CB$shS)Ue^>JVpVCO=e!ga3Uyd4dEfcR&eYoFCghf;j>* zEeU10bXrF9;U43#CAQk;Q#1olI2Dhy{P!q{wHL+}wkoF7RSrB{TrV94^4d|=QyiFs z&^@V_&|`MJyf)0y%YTO{;%QH;1k;Q?ZhlCg-+E*9&D+q-JIu`Q2@Kd6Li*bFE?Nq1eu~E) zClF`5EEX;d#_`hLn7?$A)XQkQM(ZPTBD@VT1*4*ar zAJN#tO?9d7KYsL38>8_S)&ut>e0E3ex&r$9`>l@-|JX%DL1&?}CrQF@&`w(3`@!tf zw1mUQRnap`%JVf3e}FpB>MZWzuzU>6x3S9S`-f+?A)`fJ=H}+PBe2mWkYNj494Sl3 zLah6UnU{h0*JYZ=N}DE%pjM@qbPyNU4#-++AGu~Bq2(aE6y=dG5Ixt98ikqs=|`CcQepBnl|Xe;+K=v)Nr4?a!1CeJ<_%BeGKgiCEpa z!_F?2t&V}tcc-#QNi{x5DjhZ{8xc1e)~wev*nxNGTeKE1HzTy!2_AsT=17uyD^ivS z@haGoDPOC7DoHIa-Wr9QDm9s@fFr}V5>UW7;8UE$PbbRnNO;%mheLDqQ)4nu+HS!x zV;C=}c`rkE=jiLYGU08DBtvwUkeWABuKdgK~c*3*_R6w z6tPDbUc0oKOkyCcF*5d(jW=Q}AAdwB0*dTUl5^5HH@qQY*%~5{MxF|l0Lj1Sy~P%l zdB31+P(9>GiLnRdHR?`;<9+lwO+W1sUPo6uEHCiYsS&DL+T;Wez=36C0E1{D5mx3Q z-pg{QskfH5c$%cy=W{JJ-p2~b(NIGkCrX=)^E#5dK}(OCrEV>a>H6I-bxA6199Xr~ zexHM;9}bL?5?oF{M+8}(kd5wWwbx+IHkg!1q2qtDoXHmPR4w*^aV8L8$-x0{3Kz%> z>J1q0-HZ?~mMI{^qrB|)fEwl0Re=7a^FF;4IjDVa652iXROd!yC4?7WzRV>)hq;C} z7V(&Q@=ZYMuK>&Y`siu$xA{uq%{5yt5xX;lI^&w+vL&_7yF)C-sG4 z&_6zbgoGM!|G2(M`qEeqW}m>|U%&-a5|t5kG%7OM*OD_}h7d14(la%^9>ZHS;I-rZ zXEuS;vv$ZUb_Gmw6Ciy>e;h9q#9NTJvewLknx1-Y(`mMa~ShS9nh)bEaUUYI~t zmrr^lteBS#L}|^7$2CUiA;m@cdQ&DHZMUZ@flUD+z~-H=SXV889=xoYK%WL@w-&_J zWfv`$X%=NOfNj{ZDjTpR;GhCStwCt@>jHS`>#4ki`)mgwVI8nxIa9xz>wxBs@(i9w z-ouJg=Gp{54GqF&dJt_J(pPuhT?&sI1b}rp+5V;)tp(8%>3Pf)jNphm=DzhHLl4L% zFksXdXo@~Q%(BS5_Zo%$?B2*j*U16_2>veMAWG$i5Zlmw2v{*ol522U8PyT0r709rpb1rq9W zXdX?8m34}tkt+2>{*No4U_pc$&dW}#C3H1ETyA;2kNnw)e*SJol?PVnuiyv8`JJ1As$@?IAds`3GX%n3PARe()pNArs#Gig< zX68x>VcxoP8N&QyDWv`k*pRo06$B!nnJUTHg33OIC13kDiQHN^ClsRHD45eDXa6&!v3^mgJi4Mln2iqe-5eKSEy`}@Izodu_giv$dmIj>z zbC>O4;_*8qa05L*PC6Qcd&{BoKilKF$l+XCN@TD)i`zTB6!wIUsL&yLvVb8>Z011N z>Hr+=vddm?5a~X)Yzr1<{n#b06?fL3zj;b}e+beK~KfPt;a?m&d`}zYmPY zoUq~Rp}_KUt2|A=zekiQZXyAZx(R;FGu*grpddU(XBl4ubL0@vYZs7S6&*3WotcY> zL>zYnmX0QP-h>#PW*UkXhh?zxSr+wrKaTi*jku<0oE0mR5OmB`2| z-le&z>^0&LMhf82$>acQo>5p2v^By&6T;tj{ve9XC;qu?Qilzjy31FDEU^W~Gi zh=>c9C08v57@FQq@WlJxfmSX=^qGY>ZDi(4axc{Re;E7C_B#FYKISX^O_Z_x zOXonNQ6E|e{^JTE^su?sa?hj0yw0|Q@S`m6jH0uAH6swLkiHg&YUEwr~I!^K#EDTUj|%apoXj>Upv&j zJ6y%q?Gjf9>wz+3>79cjq44~o^=V%h!~h({j^0hbj3CQ?xAN76{B51}ha-=#N0_vj zJ~x?<6Fk(wqVTGwJPtlbkVOf>&!6N-4%pYZzd&-KY+t_b2!HwkxiGc^0JRi|w5+W2 z{VFqi(Nuu8#?T z*}n`mR+N^T(IU2mzLG^0k!J5s(yltc>?zP)`zUUnwfcMCat>Jjj3GIWRkp4O`V( zR4nz!oycDer=5Gb8s!qegUI^+=W$^z+jVR6k z#+1P6YT#q}EAP%BzJpzn*tJ}&4cAO0{s0;oXz}6CFJRjyz*!VCIE8C1$gdaq`H2!D z&+5@d7@{)*zj{?M!v!zqeD=#mR+jjrF$)U%4n&}6b!qZNgAtLT#@CIdRl7z53(FTC z1+ZMOtSsF$jSlt#k;&9zr(0zRFYQG|yQ#;#OJ4#r4T(d{z@t3-!=`zmmVzl{B>wBg z*tP}rt!~`_BS*wJa8c@ABgp0fr(qI9wKjGcl-n<*98f}12dD-KKyeNM9i*6c=)6DD z6ZEewbk^Z3O|Ohh1M6)ULa{tMJ*M3l2yl?!vXx-KWts5XOS3YIbYbVDE7z`d9RAtr zLi_6Pwmy`2z{`Uf-Wurd{}LbpjvqSy-q)~n*4I$?Rq0RBCh>{_nca`i(Fq^Jsgil5 ze$PBUX>mBN^80!`QKxjD8a;mR=`K}XPohA6tNZVbZ&zoDqBNmAlGPIuh^1Rwb6s&~ z%@d{KXp7|<>-N~^!bDH?TJ05%lRP~BDr0$?%LLjAdBrzbLv)lpNngq_9^zpO>mle$ zR@yv4(Z$TWs`#IQ!O{pG_;GZwo{*pBtGcF5Qj5A&gk$mG3i2h1`yEZn}`Hs!5*enU1nMu7Wr_l#vRvHZG zG255fd%ZCYDJj=4dkejYq7nEs$i&Q?4UJv{-G2J-&c_W4jY)SZp#*Up#e65g{rnHV zD-^EmgQhbBT|GU{(jgBJOqMn7LZ6@-Q=w0XMA+Hc?IiVIY7C%T?Q6!$_DF1KMVPa{ z9sBiw;H?a9*(~cgA2GNm1D+QKztRRW|vN6W-I}tgS_uMx-Y}~raFO_Hhld;{Ig&I z;PQ8`B-|T%&P&Vrr!7J43%81T8tknoYo61yL4?fUy|TuOyS_^< z)phK#Q`2J^?M?VDGgmAQ+6plKsY?fc^}q>7l}q!!$s;>gHLzrKzzXr)Yng<Svm89o7*I%z2( zR)q|sNmNYd!>Y z!`m-CR2Eq>bObkT0{CW3PmY#y+}bu zbM0tLew8FUaKUCQOx6up$C;Ev_iJEtr8EF})BXG)pq!;~dkGk2FsW!m8CC$O&uS>G z7qc8hm|cTEA<4e0_wXj(+BFcaPGaUmfziSAc2-g%XMh!ZC-!IdPHTaeUlJg%Dc6RW zmmlO`IgmtTv~3nsK}sO`%|=B|yHHf8XAB&?vX+dzXYMrx%1MF_m!{}EM2ybESEtOH z{6<`JK@o31$}8yu3u|5Z4gLLb5DKTAC-A@N_Pdv<_D{oB?nWNN0v6;&WtS2wD9_AsPtIZ#!*gIGrIZ}V@j4a8x5fgs$LU)+#^KjMIu&6`fnbRs zLVjQ;_Ex;FP+V2+@B;$!l95J93KEhxH@<~3bfBsyS)09mg)Ik}15^b-Sz?QnmFuf# zB2%}jC*0M{T;8MUDZW-Z#F~DXd&cTTf9E!#zBloo7N|-7r?YI zJqgYxp!eC<`E!4jmPTliEzdlFh}>Ntb8>QEG=sLAZ%eyc{4;%PPOIa z(&l(=kbt7lv+y|j0G@F8+X>Ljbe(%+-r`UfsuGZh`5AYyjD09PzGeDoSH#D0&BOgO zGkNk&S|Jyw*4v?{-j5pek*qcz&zINi=cK1@9jEmy*lXWUx$+Phh)<-vhijxf{q21Y zsL;fAYNT?D%6y;I1w{d(CfEu#|Mf>{{Ob%oog3fYrt{(C5baKmts8uqO?6*7laFB6y8~?b-zqmVY(~;B8$DS@n`Gz1cs7I>&7N=gI z+cA7giHuGt7W7agfNG-`*t22d&2^($t+>3leijDho-`@SU(aO0&kqizSDT?yGaT`>VfMfgD32d+_i<{v`M~|FLsx@7jF`hJ1j`60xi@9sC3qsQJ_kpXp zIc(zEK0Vj{lC>+TeW*IB>>hIw+tT)g@Qv=63URn>_hWz=UfI^uMHM@ZH7mCoe_l7^-q4Kq?vSHG}pcsI4$d0g1@7nv$Y9UID``G2+d-S1ew z|NoI$36)KhRFYM8;YN|Htn8KSkeyA0xKZ}VmQhyOTf->XE4ysj^X9g`&r4pP*ZXrE zpMT(c9PfT8hpzj)uj?Go=i_{ykH_QEb-^q$ycQ1%415f=oz+jBZ&#Q4bi;j|2`9hX z@XKI%)zl&~lb+2r+@;cvvvRyYfskRYooyN{RoHZ^(Q$<1bBmD!U{7eU?bK|E4>$=( zof&gpN*_#&TV1u?wjC7#CE<{4?E>#ri3&ZpDZ((?JIVt^uQ#V1Y_!(0tY1NVW=a8b zmHZPkRxgsQch%E%g}-gx{CCvXUx?3|;trZd(9WP4SXoFohv!ly+_(I}l6-L1Q zPsHqW0j6cP>kJhmr-{=HQvyd7sv-l!y+mHjCl$=UMf)1(>Q=M#di!9g|%}cC=4ZoeS`Y zp$th7`Nw9iME_Ey=rksr`NeSi^HIZ(p3Aomm2=&!cSI0%p$MB9Un6t&pOX? zyUYn=qvlV-u5v_jbnORo3Ll%B-`a#<)-|%%sk|RT&%GB>Xdk=Y+VpDVIAM&L@@oNm zp*)X)+&1<25Qd*QSI;yJoyAEano$bh&EE{lyCMwari>lCGG&ygO8dSAsAbZN1ZCoi zO@FugJ(zn1Sn@)+4G!)I)z#b29-L+nxJs)ePk+leL*j+d3%fzA_P3VrVUP^(Yf5^~ zuD0y_4h#yMP}eBGl$o&{F&hQO9yaf`lX))HK`| zO7G4+tdi;$b5~F$TYg@b9e$;SFac|Q+KIUPz22xBDwjExejm}K=C_$~14aP`TiJEJ zf(M9-3OOD126>XdmCoG*ZP99u+c8(ZN3~yfDaCbxv8+7xe?rJ}}>}y=SS$MEp?bz%8g@FK7N&n-$w~ zf+xUMKmnC$OhteDDr3Gj3!Pd{1h7+XtT%kag4U+V3)P)?^yRplJc^a;21MOP3dh%1Giq80uwyU^|M99W5BJRBEv z!?cQTRF2z}NuWgU{H%4&hf6Ds4by7A_jSGH3TMlpkbpn8KG~~DFzDP;dZ5nD9vw{lW-wH>M>bP!j&?M3#JTT@ zWKWHS+D-8RP@5o83{fT_S)~zV)C_$rpi*%nax1rs21A5b)#3yp=gpp*0*`X$;SSua zw@T*Z<+tf&X=2?Um2l>KUN|)0Fd7e}FfRS_$*?iTDeSM<4Y^API9}+z8)^Ycn!2eT z3!oh`R<)8$_qh^lgvmNmC> zY&I&S!(bfrPX?~88DUBkT4*~rG&F1{<@moP(@B0{jl@*MQT`ovG73PlDlx2hIGB?& zetg&gN|MxQUH3@nN`g*9Bcsa!lvE5+6V{-fVoswUIVa$W&KoqgfWZC1Y$-(*+wF)gJ0X6&9tpmh3x zSbZsMxWvQ9d80Y6MeFg5oS{)WNm@(ZkpvUT*bWFXCdDJ$it-A1rZFdRQ!k3$X%*fhjfMtr!|jpvm>=BQTKhX*BzQ4Y{+p(&b6yKnvz-m%z90n0$IU; z)?BU`VF#oA8dxqpJL~nZN4jo6L(pW_w*OUvWiL4?jhzozY3U^_HrF5B-Y+@W?(y$L zKD-Vklfe=aKfXmc5khYRlvI0&`s*R2s5YMD_Pm@Y*-l3?G+J{r2sSv#pa`=v}u-1rY z22$zjWJrQBLw26>6oQ_Rh~o)BW{nwU<5*-`idH5bf)+*hm4|fEI}{ku9QG)gAw)*>Ew3^1fQULo z)_cKzu43b|@1O1=$zZy8MOFq9VMyymrjg&*o9;Or!{jQWlEn5NJT~0C-(O(+HsU;M z6qL@hW5jjGNd*2%baC*s*`e6rfKhZ7C0Y;nE4G>{OFH=B+YY*8KFrmBg*PCrI`*5Czj-JMxX@^dF$S$u^Pv9gzPq8bmGN@t|8{xFtRo z7EZp6)(wlW)~!tS>@Bx5p%L|Pi{$Us`_VE0##*d`O1m4__J_IY5Xy?*pHjO(b^%9+ zA3K2dZ+q4t18xfgMP^~IUn?%yS>>NG;1Or|YI!JQBMb0?Dh6)W&>8QiC)5q>vFo#| zV?mq`ca_e|QoHBR%$bz~nT!t>rJ&#&)p1=SU)5i0(NoQv3xks*u1ma_9#Xye14dZ` z-wyose@XOq*^};tmt}-S#9gx?CcerHo4o%~ z>D~Shm0m1Ka$BVbQ=0!qimZo6&Gu8{m9hG%w!)7e)5Q*ILadl04@Fr2YP)|u1%8`3 zFPvdslZm>{%`63uU0`KyqS zjLgjDzx<{?bl{)0HXa={2f@fgjq5Tfl)I-%kF_|}p!BJSD;(bcNVwIfqMbPbXaA%A z-CP3joCJ9kxn}M~A%Ul?ZI$w$dOYV+=I%H|)wL$vV%ygfP4CFA zJlNpW$_KJg_n61Kb~3HEfnA7K&67Ld{5W&Q^@hCBtAh64?>=XZ zWPc<<&$m@4ej$)diMIoVBeJGHfWKCQQqo6qBJnybzHoqYt-}d}B0h8NvuxfKx91)v zI8Pb&xJLX19jF-qTkS1nxZYXBPjss#J)W7>YDPD5P9gd_YFKrKUh=R1hKjEbpwcJd z7AOhSPzvj6cCfdI!1r{|>wVzcq1k0)a0*f5O?#AyyR27VCn(OmcN7bWRN-^)sHd69 z47|ruFP;^ThV21IwQbg^3WJCu^FH~I$Vh4M0Xc^tFi-@@%SA%Wy;H_FbRIb8ho8{o8nGKs z*RLXU(g=FNi!YqFZO683&fbYHeKMP@sZ0UXr(|zrCuuaiYV?uUq*~j#OP$94izyH+ zsdm0Af*I0b_{l>2$b7uLB=GDXoAf__VGH&je?u8B+rOcV7y)Icv$=avbsC6*#a6#f zcY9_SbID}(M)~}o_m7ujI>KBw`&-{#E&gi{@sEjd9S&|YRgG1~RLNXtJ$lN1(I!Q` zi8)p=WF5RZNJw-4(R7m*bs26^50aE!Q zSnw;-~0Y12^5n!tURJm zwpw`exJgr1Tyqkbr4e=?$U>01Dp{>QzEjwJWv@%;Q^>z|uBc;nAfOiqFrCYsDTW0Eg8 zH1w%lnJ_Mqx+iA+cq;5iKH7B|)3|i0-@HGNY#`xvG)Q2-?b~tnk#a;Wo;^0#??FPG zD}vs-wRO@Q0jJ#*vP(}cc%;cui?=h^pIrQb3Ss=~En>xct1vYhQtt{W>1ZEYI5*xw+LhSskEcGTgc&HGu z(`e&!K++5t><`)5p)`M*3kVN-S|u#*FMj!D0F&PTR&++ar2bl5vGE-hJyyVg?)l{d zDtDwF@dWgO{}c>Niu2k}l~+n&)G7W8a8my_p(D^Q!b9u!#k71Tu=$@&6LcRK(w+LT zu*Xs`mRkxF+ib8G?dMooR*GOrnDyi_|JwL$P@T%XTlWbF>4_1~eF(wE<-O6DKnkU& zzr0SB+vKc6S;5CbuIF!$KCOkUv*&*ABt6puZ55RdBI})O{&Csmxr3!t7Ts^lW(p7i z);rjPf&}R{Qa$f4cMj5!sF+Rbp`fFyXSpPh|FIi!n1EG83gKC}{Uy(Z5?rKg6$N%U zT76SZu=kDqcNbcu?VjLE&;h{`mBVR>OT+4&N z)z!BeL62cedYl#JOPLJ#%qW9p$rJ7J&YZpTHyqLHU>>4Fl5*r#BA^af_*ZL+dF#3M zf^xS{ePZ@?O@~o`YS{OGHlt}N(iI{_9_}W5&owW2p@MOHrwp}hd%yYRJn09S-l=Mv z9Ou20|IE*B;6NY*++T{`JxiRg!H6Pi>0rNtm*>aMXeC6ocwxdSn9&#es<$P64dimlo!d68+9y zgKhY^YnfhF_cHQLG`BY|YJj|*<6iC89A-;TT`K3zJlBc(#>7{i?jOa#vUy5@N%PQm z*bBvu9OIzI&n%s!AF5Q7bP!OBYp8FY$51wV3%mY+lLV>_o?0)&rDQ(L0v}}0QI`Y$ za8w|3Q4YuAT206+tEN^wmzUhfj?pwA?+A#3vx=KWm`{vDmv?G^vM^g8yxx7(%UIq2 zO!LJdl0)IaSCXS9qZX;2O*uJztCC{mPkVDb?;s>1G6HOX65d-`Ono zlKa~J159ZgB~%*Za4V!WK0ykxslQhEp`#=9%j%S9B7b}|@00Eal#>h5AaAv(AVIut z4_5>E1@tO46B$!-*NR`MxrJ_Ds?>OfoY8!GQd766h}C{=SwVZ1s7mIdIV7t4pUJ=^ z@^W(FMaumRLFbKFla>sl3M*~B-#j*0;T!vXwX3lNEq~2Wk{uxk4wTzO`s3?veMwv0 z{Stal+Q#NCn}e_^Qsmoeb+B#b%#{ASE9May`ErH_veC7(TaBR0dC?|(1^cPzw*Hw{^U4=27(CiFmh zYRE31UerTY$bRl}^eLL_ROy-V!9h@s41-Smk->skYLY%!K=tZuZ zILur!BWK!w?7VPQ~N`4y)?6$ocCV~Y~Dy&Ml`1ts%-w$>}Z?FC|oRVlY zYZIZ!PA%gEz`cAhSc%bp|8D2>FVVZeBG*JIrk=S|Ply|+sG`}pn5j%|Fw4E_MjX-U6FE+=$O=xtlf`qj_%wS!wwpgIXah7WQ(`2(D_^Xl#+6WDM z;`VWBQ;%v_3FI9_6F%xb@47zfXZiA&cNAsW>>AZq3(njC|Js8+>Kl*m$!Jl#eU-jx z{1w}BkwfbvH6acz9+BJCc3T4;ow%KHA5qGK&7X$PvlxB&yianLU((gwFxPhMV^AR? zV`?|?mWt*Gx|LtS_poZ~QBtYV@fK#Tg{-u(g0mr^Z9fvaZu{Ig1MRJgB~N~y;pa$T zl<2b#h^J)#b3DG&_#E|zE_s^;r>KT>(fD5Umz$Sy@%~(0c9L?xUGwZyRg8$c3xigV zN)LXb=l*ug{;Wb{spm@9J113;QG&OCcht$WOohZV&5N32=Q}Fqb%T4F<^}D>)EKK6 z?e_QIf9UDe;(s{35p~S_J5d$3E2%z~F`m{se|X*L)x!)Qp&MDaJM)#+Z=U_ew;7tv zBrzA|k@o`a7*UZ5wOs9$)t}^PkH-B-R15n21BtKeRtYS+{o3CeQdRp}cKnJ;j#Gt% z7b=bD_S!P(*oZ|9{lUuDv%#H4dpT)!dI@f}Pi|Q~p+6`!agi5e80Nj}h2q86yoSzT z74TR;ztY_E{=Fcb3vU)KD|)2niP`Q36-Qw86}fc_Z2|{A9{FS&e2?83((y{YZS&ik zP0Yc!h=durylHNlG`tF(r|rsMq&j;#Nl1U;Cl?GC?P2eBx0I~k8lg34qxTh|G_Y@A zp=!(;b_z-W4j7qBiZooNgQB)vr=kD3Pi4TfH_{<|g`N!FAFx-)XF7*RM3z+mEBp7i zj$-VIa|Y`XCpiNcBty_2c9zsePn!lG^Ufe6lZ<~vOZbHBF17G#S#3avYe?|#G)WS5 zEk0T3w|6Ir(o~Z!?7feNcKFRn$W6_042rTdOWSV*pF+V%Fg03Og&xrO?7PG zWoSO${t-zKP>8@j&4*vz?z5?nT+HGbCaz@M&of@D1J9U=;ih=Bu=kTI;I15|GfoJf zm3+^25(_u1is#$$?uW0(DNtPq%_t`y$BPq({tPsuFAyV2J1%MQ1&@d)8SJ0A&HA+M zkW zft4#kDqQq(PQxUD0@RLyAc}IFP3SHEVA$AO(qWok> zzZRbWK^vdoxcX_YS@+qT#{>K2(}`NQY<0USOr*N_bEP6 zCpoJMq?0g*8ubq~bRI z5yK&RFbZ0hfk?4X5v=KTt&*IsjpSukaT`SpaH$)_2pLqgWI6|4>CP?%lIhgYW~=Lc zx3JPI9mp@7{V|i`f1Wlo$-~9c)r@}6U7yz}luFWWvYVW#9gXx_j-!+DdZe0e|3qis zX3*mk5tH@&_~ok_z1?qBZUU3EM8>o0Ga~USTWO#ha~D&V@v37ahNmwPy*bcd5!Rcw8r!ibauCFu^V36WPHg*H%$WNmxRd2Y_0`(5 zN!xt099X1K( zw;UDWl;fLAYKhFreDouA!12tVLq_~e7T;i zPNweOWp%$W`|{r#MZNuEy5lO@td>_)Tb*9$%3gUgt$x~br(m$3ora;Blfmmc2P?VeYYU4t_x$0OnXFPj_Hr}su z-GMS%LCraxuHQ)sXlJ+v6lW|MnSwl=C?&gUqmP?xyHBQ#>y#VGW^xTs@f70`sR*Gd zaq``s<_kRS@2RkDW-l!g-pZ`x(sbL8mUvw8wFG|D;`MS7Q-rINbblNKb5D=;6g7!;*pikCcgTm^&bYj*Em z>RF#KtI*NO(4szvf#D`75p6|peq}cldeKpnoXvttzVO-f&tt5tLQ>kWPOLIL_4UMz zK@qg$=Jsebm*#DQ>i7Nwk)@;i_v2c7!3~}CDx{HjAjmWRp`frxBWH3=S%Cfg_g8VO zi`nziS?0A{rK0b2h7<&$U|R;*$ea|z%CRG7Nr6FAVkXjGxO{q!U+nt84u+1?6no}X z$fL|(!>ZKmBWm=qoHf@YIN29lgwup=nq%Y*ia1TpiOEc3xCVNJ(^&DMmby6o%T|To z=(z7s`*mF0%{a*1NOAK5zJ^{Nkb?Hy2RybDI}&pWf!_E4u!M>h1g@B*kk6(o-9N{E z->NsdJNxp(thewi+Efpo+E_}Vg&o&)Wnyqu*|Aw6qx!cW-B`a#Qf>k{yMM>}^cb;E zeaGta^u4{D`s$Cf@P4CFpqN#hN}Fw#lTv78Ph8(w?^3}0F{?;;(Xo%Ez)Q@CU-^>k zd(7L9IIUht$wNTbY3{y_#uEjRgPk*yK?7rE6?ZEW_S|V@6ngu|zSWtzDrd;fu4Bym z?D#i%hF#1pg8t9!=zZ~1$$BKp0e>7GJrhMp z#Pb4LKG!9n??Wa@i0<`+>4E|*fpbMJnotr&%|NZD#nNsL5AeR_*-`@=db72;60Wg= zYs#nlW?U{Xp*L}xwTQ`PR8Z8#@K$Li^nfT}#$`eTGd6~fFng4{6uTqA4q(HH{~IO>mx?D{w5 zNBuf7V_wHkH}NX>rQ;S({dmbg>d7A>G<(OONP7Bb0a3Sveovdnc@uhD^{Q$|OX0XK zNgv{QUJo}n^Ua)}7M*b2rzv?Scw|rH8Ar>Wc%0f<@Y`y<8-&&CqNLN*-)eeo)4;3N z5VJD=qQfsyI^hb}7Oq_=_SuhGeO+15D=yzkBa9?}w!b}Xh+S!HWN{igA5Thi5&h8E z*dAv8r!%JITou@9w(%mL(=Qu6*VAHdJFn`OTa-S+oz}Bgyp0+2te0ME&EI&^nBW)~ zx>#ym?~;8BW@BKTp-gl=gPIN{;%|LF{A9Ya9)!@)7=HtoVo{Ugmi!U9I z2%75oqeMMy_ZjEI!sZ~x{MHBpVU5q2e&dR}yu3I^^_4*nd;bl#zpVk(oQ|YsHy3(@ zshi=QP>jvdoS}1+*8?2r_`j}enACH)?uxi&$YgFN@^JfEmg;~^{Wc`fEy9O_uVw-J!ewlb9nRWXeNj|Z{y%bl9~2% z#jSTm`>uC?!Hl|cDhcd`r%14IvkGx9;{vjoinp$jmY5C8wr3q43BOYRP|I3)IiVda zO4%iQn9)0Dg state.todos], @@ -93,7 +97,9 @@ export function autotrackMemoize(func: Func) { return cache.value } - memoized.clearCache = () => cache.clear() + memoized.clearCache = () => { + return cache.clear() + } - return memoized as Func & { clearCache: () => void } + return memoized as Func & Simplify } diff --git a/src/createCurriedSelectorCreator.ts b/src/createCurriedSelectorCreator.ts new file mode 100644 index 000000000..f842e6ee4 --- /dev/null +++ b/src/createCurriedSelectorCreator.ts @@ -0,0 +1,132 @@ +import type { CreateSelectorFunction } from './createSelectorCreator' +import { createSelectorCreator } from './createSelectorCreator' + +import { defaultMemoize } from './defaultMemoize' +import type { + Combiner, + CreateSelectorOptions, + CurriedOutputSelector, + DropFirstParameter, + InterruptRecursion, + SelectorArray, + Simplify, + UnknownMemoizer +} from './types' + +/** + * @WIP + */ +export interface CreateCurriedSelector< + MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize +> { + /** + * One arg + */ + ( + ...createSelectorArgs: [ + ...inputSelectors: InputSelectors, + combiner: Combiner + ] + ): CurriedOutputSelector< + InputSelectors, + Result, + MemoizeFunction, + ArgsMemoizeFunction + > & + InterruptRecursion + + /** + * inline args + */ + < + InputSelectors extends SelectorArray, + Result, + OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction, + OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction + >( + ...createSelectorArgs: [ + ...inputSelectors: InputSelectors, + combiner: Combiner, + createSelectorOptions: Simplify< + CreateSelectorOptions< + MemoizeFunction, + ArgsMemoizeFunction, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > + > + ] + ): CurriedOutputSelector< + InputSelectors, + Result, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > & + InterruptRecursion + + /** + * array args + */ + < + InputSelectors extends SelectorArray, + Result, + OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction, + OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction + >( + inputSelectors: [...InputSelectors], + combiner: Combiner, + createSelectorOptions?: Simplify< + CreateSelectorOptions< + MemoizeFunction, + ArgsMemoizeFunction, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > + > + ): CurriedOutputSelector< + InputSelectors, + Result, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > & + InterruptRecursion +} + +/** + * @WIP + */ +export function createCurriedSelectorCreator< + MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize +>(...createSelectorCreatorArgs: Parameters) { + const createSelector = createSelectorCreator(...createSelectorCreatorArgs) + + const createCurriedSelector = ( + ...createSelectorArgs: Parameters< + CreateSelectorFunction + > + ) => { + // @ts-ignore + const selector = createSelector.apply(null, createSelectorArgs) + // const selector = createSelector(...createSelectorArgs) + const curriedSelector = selector.argsMemoize( + (...params: DropFirstParameter) => { + return selector.argsMemoize((state: Parameters[0]) => { + return selector(state, ...params) + }) + } + ) + return Object.assign(curriedSelector, selector) as CurriedOutputSelector + } + return createCurriedSelector as unknown as CreateCurriedSelector< + MemoizeFunction, + ArgsMemoizeFunction + > +} + +/** + * @WIP + */ +export const createCurriedSelector = + /* #__PURE__ */ createCurriedSelectorCreator(defaultMemoize) diff --git a/src/createSelectorCreator.ts b/src/createSelectorCreator.ts index 85525ca9c..ff9d46230 100644 --- a/src/createSelectorCreator.ts +++ b/src/createSelectorCreator.ts @@ -1,4 +1,3 @@ -import type { OutputSelector, Selector, SelectorArray } from 'reselect' import { defaultMemoize } from './defaultMemoize' import type { @@ -9,6 +8,11 @@ import type { GetParamsFromSelectors, GetStateFromSelectors, InterruptRecursion, + OutputSelector, + Selector, + SelectorArray, + SetRequired, + Simplify, StabilityCheckFrequency, UnknownMemoizer } from './types' @@ -82,7 +86,7 @@ export interface CreateSelectorFunction< ...createSelectorArgs: [ ...inputSelectors: InputSelectors, combiner: Combiner, - createSelectorOptions: Partial< + createSelectorOptions: Simplify< CreateSelectorOptions< MemoizeFunction, ArgsMemoizeFunction, @@ -122,7 +126,7 @@ export interface CreateSelectorFunction< >( inputSelectors: [...InputSelectors], combiner: Combiner, - createSelectorOptions?: Partial< + createSelectorOptions?: Simplify< CreateSelectorOptions< MemoizeFunction, ArgsMemoizeFunction, @@ -220,11 +224,16 @@ export function createSelectorCreator< MemoizeFunction extends UnknownMemoizer, ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize >( - options: CreateSelectorOptions< - typeof defaultMemoize, - typeof defaultMemoize, - MemoizeFunction, - ArgsMemoizeFunction + options: Simplify< + SetRequired< + CreateSelectorOptions< + typeof defaultMemoize, + typeof defaultMemoize, + MemoizeFunction, + ArgsMemoizeFunction + >, + 'memoize' + > > ): CreateSelectorFunction @@ -276,27 +285,29 @@ export function createSelectorCreator< ArgsMemoizeFunction extends UnknownMemoizer, MemoizeOrOptions extends | MemoizeFunction - | CreateSelectorOptions + | SetRequired< + CreateSelectorOptions, + 'memoize' + > >( memoizeOrOptions: MemoizeOrOptions, - ...memoizeOptionsFromArgs: MemoizeOrOptions extends CreateSelectorOptions< - MemoizeFunction, - ArgsMemoizeFunction + ...memoizeOptionsFromArgs: MemoizeOrOptions extends SetRequired< + CreateSelectorOptions, + 'memoize' > ? never : DropFirstParameter ) { /** options initially passed into `createSelectorCreator`. */ - const createSelectorCreatorOptions: CreateSelectorOptions< - MemoizeFunction, - ArgsMemoizeFunction - > = - typeof memoizeOrOptions === 'function' - ? { - memoize: memoizeOrOptions as MemoizeFunction, - memoizeOptions: memoizeOptionsFromArgs - } - : memoizeOrOptions + const createSelectorCreatorOptions: SetRequired< + CreateSelectorOptions, + 'memoize' + > = typeof memoizeOrOptions === 'function' + ? { + memoize: memoizeOrOptions as MemoizeFunction, + memoizeOptions: memoizeOptionsFromArgs + } + : memoizeOrOptions const createSelector = < InputSelectors extends SelectorArray, @@ -307,41 +318,36 @@ export function createSelectorCreator< ...createSelectorArgs: [ ...inputSelectors: [...InputSelectors], combiner: Combiner, - createSelectorOptions?: Partial< - CreateSelectorOptions< - MemoizeFunction, - ArgsMemoizeFunction, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction - > + createSelectorOptions?: CreateSelectorOptions< + MemoizeFunction, + ArgsMemoizeFunction, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction > ] ) => { let recomputations = 0 + let dependencyRecomputations = 0 let lastResult: Result - // Due to the intricacies of rest params, we can't do an optional arg after `...funcs`. + // Due to the intricacies of rest params, we can't do an optional arg after `...createSelectorArgs`. // So, start by declaring the default value here. // (And yes, the words 'memoize' and 'options' appear too many times in this next sequence.) - let directlyPassedOptions: Partial< - CreateSelectorOptions< - MemoizeFunction, - ArgsMemoizeFunction, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction - > + let directlyPassedOptions: CreateSelectorOptions< + MemoizeFunction, + ArgsMemoizeFunction, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction > = {} // Normally, the result func or "combiner" is the last arg let resultFunc = createSelectorArgs.pop() as | Combiner - | Partial< - CreateSelectorOptions< - MemoizeFunction, - ArgsMemoizeFunction, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction - > + | CreateSelectorOptions< + MemoizeFunction, + ArgsMemoizeFunction, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction > // If the result func is actually an _object_, assume it's our options object @@ -395,6 +401,7 @@ export function createSelectorCreator< // If a selector is called with the exact same arguments we don't need to traverse our dependencies again. const selector = argsMemoize(function dependenciesChecker() { + dependencyRecomputations++ /** Return values of input selectors which the `resultFunc` takes as arguments. */ const inputSelectorResults = collectInputSelectorResults( dependencies, @@ -436,6 +443,8 @@ export function createSelectorCreator< resultFunc, memoizedResultFunc, dependencies, + dependencyRecomputations: () => dependencyRecomputations, + resetDependencyRecomputations: () => (dependencyRecomputations = 0), lastResult: () => lastResult, recomputations: () => recomputations, resetRecomputations: () => (recomputations = 0), diff --git a/src/createStructuredSelector.ts b/src/createStructuredSelector.ts index 42cfe2935..be926cc2a 100644 --- a/src/createStructuredSelector.ts +++ b/src/createStructuredSelector.ts @@ -80,12 +80,12 @@ export interface StructuredSelectorCreator { * ```ts * import { createSelector, createStructuredSelector } from 'reselect' * - * interface State { + * interface RootState { * todos: { id: number; completed: boolean }[] * alerts: { id: number; read: boolean }[] * } * - * const state: State = { + * const state: RootState = { * todos: [ * { id: 0, completed: false }, * { id: 1, completed: true } @@ -99,9 +99,9 @@ export interface StructuredSelectorCreator { * // This: * const structuredSelector = createStructuredSelector( * { - * allTodos: (state: State) => state.todos, - * allAlerts: (state: State) => state.alerts, - * selectedTodo: (state: State, id: number) => state.todos[id] + * allTodos: (state: RootState) => state.todos, + * allAlerts: (state: RootState) => state.alerts, + * selectedTodo: (state: RootState, id: number) => state.todos[id] * }, * createSelector * ) @@ -109,9 +109,9 @@ export interface StructuredSelectorCreator { * // Is essentially the same as this: * const selector = createSelector( * [ - * (state: State) => state.todos, - * (state: State) => state.alerts, - * (state: State, id: number) => state.todos[id] + * (state: RootState) => state.todos, + * (state: RootState) => state.alerts, + * (state: RootState, id: number) => state.todos[id] * ], * (allTodos, allAlerts, selectedTodo) => { * return { diff --git a/src/defaultMemoize.ts b/src/defaultMemoize.ts index a5c873356..716c8a4df 100644 --- a/src/defaultMemoize.ts +++ b/src/defaultMemoize.ts @@ -1,4 +1,9 @@ -import type { AnyFunction, EqualityFn } from './types' +import type { + AnyFunction, + DefaultMemoizeFields, + EqualityFn, + Simplify +} from './types' // Cache implementation based on Erik Rasmussen's `lru-memoize`: // https://github.com/erikras/lru-memoize @@ -209,5 +214,5 @@ export function defaultMemoize( cache.clear() } - return memoized as Func & { clearCache: () => void } + return memoized as Func & Simplify } diff --git a/src/index.ts b/src/index.ts index 9ad07492e..ab800d4f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,9 @@ export { autotrackMemoize as unstable_autotrackMemoize } from './autotrackMemoize/autotrackMemoize' +export { + createCurriedSelector, + createCurriedSelectorCreator +} from './createCurriedSelectorCreator' +export type { CreateCurriedSelector } from './createCurriedSelectorCreator' export { createSelector, createSelectorCreator, @@ -15,6 +20,7 @@ export type { DefaultMemoizeOptions } from './defaultMemoize' export type { Combiner, CreateSelectorOptions, + DefaultMemoizeFields, EqualityFn, ExtractMemoizerFields, GetParamsFromSelectors, diff --git a/src/types.ts b/src/types.ts index c50128c77..276070c5f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -93,12 +93,12 @@ export interface CreateSelectorOptions< * ```ts * import { createSelector, weakMapMemoize } from 'reselect' * - * const selectTodoById = createSelector( + * const selectTodosById = createSelector( * [ * (state: RootState) => state.todos, * (state: RootState, id: number) => id * ], - * (todos) => todos[id], + * (todos, id) => todos.filter(todo => todo.id === id), * { memoize: weakMapMemoize } * ) * ``` @@ -106,23 +106,27 @@ export interface CreateSelectorOptions< * @since 5.0.0 */ // If `memoize` is not provided inside the options object, fallback to `MemoizeFunction` which is the original memoize function passed into `createSelectorCreator`. - memoize: FallbackIfNever + memoize?: FallbackIfNever /** - * The optional memoize function that is used to memoize the arguments passed into the output selector generated by `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). + * The optional memoize function that is used to memoize the arguments + * passed into the output selector generated by `createSelector` + * (e.g., `defaultMemoize` or `weakMapMemoize`). * - * When passed directly into `createSelector`, it overrides the `argsMemoize` function initially passed into `createSelectorCreator`. If none was initially provided, `defaultMemoize` will be used. + * When passed directly into `createSelector`, it overrides the + * `argsMemoize` function initially passed into `createSelectorCreator`. + * If none was initially provided, `defaultMemoize` will be used. * * @example * ```ts * import { createSelector, weakMapMemoize } from 'reselect' * - * const selectTodoById = createSelector( + * const selectTodosById = createSelector( * [ * (state: RootState) => state.todos, * (state: RootState, id: number) => id * ], - * (todos) => todos[id], + * (todos, id) => todos.filter(todo => todo.id === id), * { argsMemoize: weakMapMemoize } * ) * ``` @@ -187,19 +191,58 @@ export type OutputSelectorFields< MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize > = { - /** The final function passed to `createSelector`. Otherwise known as the `combiner`.*/ + /** + * The final function passed to `createSelector`. Otherwise known as the `combiner`. + */ resultFunc: Combiner - /** The memoized version of {@linkcode OutputSelectorFields.resultFunc resultFunc}. */ + + /** + * The memoized version of {@linkcode OutputSelectorFields.resultFunc resultFunc}. + * + */ memoizedResultFunc: Combiner & ExtractMemoizerFields - /** Returns the last result calculated by the output selector. */ + + /** + * @Returns The last result calculated by {@linkcode OutputSelectorFields.memoizedResultFunc memoizedResultFunc}. + */ lastResult: () => Result - /** An array of the input selectors. */ + + /** + * The array of the input selectors used by `createSelector` to compose the + * combiner ({@linkcode OutputSelectorFields.memoizedResultFunc memoizedResultFunc}). + */ dependencies: InputSelectors - /** Counts the number of times the output has been recalculated. */ + + /** + * Counts the number of times {@linkcode OutputSelectorFields.memoizedResultFunc memoizedResultFunc} has been recalculated. + * + */ recomputations: () => number - /** Resets the count of `recomputations` count to 0. */ + + /** + * Resets the count of {@linkcode OutputSelectorFields.recomputations recomputations} count to 0. + * + */ resetRecomputations: () => 0 + + /** + * Counts the number of times the input selectors ({@linkcode OutputSelectorFields.dependencies dependencies}) + * have been recalculated. This is distinct from {@linkcode OutputSelectorFields.recomputations recomputations}, + * which tracks the recalculations of the result function. + * + * @since 5.0.0 + */ + dependencyRecomputations: () => number + + /** + * Resets the count {@linkcode OutputSelectorFields.dependencyRecomputations dependencyRecomputations} + * for the input selectors ({@linkcode OutputSelectorFields.dependencies dependencies}) + * of a memoized selector. + * + * @since 5.0.0 + */ + resetDependencyRecomputations: () => 0 } & Simplify< Required< Pick< @@ -237,6 +280,27 @@ export type OutputSelector< ArgsMemoizeFunction > + +export type Curried any> = ( + ...params: DropFirstParameter +) => (state: Parameters[0]) => ReturnType + +export type CurriedOutputSelector< + InputSelectors extends SelectorArray = SelectorArray, + Result = unknown, + MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize +> = Curried< + OutputSelector +> & + ExtractMemoizerFields & + OutputSelectorFields< + InputSelectors, + Result, + MemoizeFunction, + ArgsMemoizeFunction + > + /** * A function that takes input selectors' return values as arguments and returns a result. Otherwise known as `resultFunc`. * @@ -335,11 +399,11 @@ export type MemoizeOptionsFromParameters< MemoizeFunction extends UnknownMemoizer > = | ( - | Simplify[0]>> + | NonFunctionType[0]> | FunctionType[0]> ) | ( - | Simplify[number]>> + | NonFunctionType[number]> | FunctionType[number]> )[] @@ -358,12 +422,12 @@ export type OverrideMemoizeOptions< OverrideMemoizeFunction extends UnknownMemoizer = never > = IfNever< OverrideMemoizeFunction, - MemoizeOptionsFromParameters, - MemoizeOptionsFromParameters + Simplify>, + Simplify> > /** - * Extracts the additional fields that a memoize function attaches to + * Extracts the additional properties or methods that a memoize function attaches to * the function it memoizes (e.g., `clearCache`). * * @template MemoizeFunction - The type of the memoize function to be checked. @@ -373,6 +437,25 @@ export type OverrideMemoizeOptions< export type ExtractMemoizerFields = Simplify>> +/** + * Represents the additional properties attached to a function memoized by `reselect`. + * + * `defaultMemoize`, `weakMapMemoize` and `autotrackMemoize` all return these properties. + * + * @see {@linkcode ExtractMemoizerFields ExtractMemoizerFields} + * + * @public + */ +export type DefaultMemoizeFields = { + /** + * Clears the memoization cache associated with a memoized function. + * This method is typically used to reset the state of the cache, allowing + * for the garbage collection of previously memoized results and ensuring + * that future calls to the function recompute the results. + */ + clearCache: () => void +} + /* * ----------------------------------------------------------------------------- * ----------------------------------------------------------------------------- @@ -414,7 +497,9 @@ export type FallbackIfNever = IfNever * * @internal */ -export type NonFunctionType = OmitIndexSignature> +export type NonFunctionType = Simplify< + OmitIndexSignature> +> /** * Extracts the function part of a type. @@ -466,11 +551,11 @@ export type Distribute = T extends T ? T : never * * @internal */ -export type FirstArrayElement = TArray extends readonly [ +export type FirstArrayElement = ArrayType extends readonly [ unknown, ...unknown[] ] - ? TArray[0] + ? ArrayType[0] : never /** @@ -478,11 +563,11 @@ export type FirstArrayElement = TArray extends readonly [ * * @internal */ -export type ArrayTail = TArray extends readonly [ +export type ArrayTail = ArrayType extends readonly [ unknown, - ...infer TTail + ...infer Tail ] - ? TTail + ? Tail : [] /** @@ -564,7 +649,8 @@ export type UnionToIntersection = ) extends // Infer the `Intersection` type since TypeScript represents the positional // arguments of unions of functions as an intersection of the union. (mergedIntersection: infer Intersection) => void - ? Intersection + ? // The `& Union` is to allow indexing by the resulting type + Intersection & Union : never /** @@ -612,6 +698,20 @@ export type ObjectValuesToTuple< ? ObjectValuesToTuple : R +/** + * Create a type that makes the given keys required. + * The remaining keys are kept as is. + * + * @see {@link https://github.com/sindresorhus/type-fest/blob/main/source/set-required.d.ts Source} + * + * @internal + */ +export type SetRequired = Omit< + BaseType, + Keys +> & + Required> + /** * * ----------------------------------------------------------------------------- @@ -717,9 +817,11 @@ export type ExpandFunction = * * @internal */ -export type Simplify = { - [KeyType in keyof T]: T[KeyType] -} & AnyNonNullishValue +export type Simplify = T extends AnyFunction + ? T + : { + [KeyType in keyof T]: T[KeyType] + } & AnyNonNullishValue /** * Fully expand a type, deeply diff --git a/src/utils.ts b/src/utils.ts index ceabd2356..54579edf5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -73,7 +73,7 @@ export function assertIsArrayOfFunctions( * @param item - The item to be checked. * @returns An array containing the input item. If the input is already an array, it's returned without modification. */ -export const ensureIsArray = (item: T | T[]) => { +export const ensureIsArray = (item: unknown) => { return Array.isArray(item) ? item : [item] } diff --git a/src/weakMapMemoize.ts b/src/weakMapMemoize.ts index 5311c9078..6f5b079f7 100644 --- a/src/weakMapMemoize.ts +++ b/src/weakMapMemoize.ts @@ -1,22 +1,46 @@ // Original source: // - https://github.com/facebook/react/blob/0b974418c9a56f6c560298560265dcf4b65784bc/packages/react/src/ReactCache.js -import type { AnyFunction } from './types' +import type { AnyFunction, DefaultMemoizeFields, Simplify } from './types' const UNTERMINATED = 0 const TERMINATED = 1 interface UnterminatedCacheNode { + /** + * Status, represents whether the cached computation returned a value or threw an error. + */ s: 0 + /** + * Value, either the cached result or an error, depending on status. + */ v: void + /** + * Object cache, a `WeakMap` where non-primitive arguments are stored. + */ o: null | WeakMap> + /** + * Primitive cache, a regular Map where primitive arguments are stored. + */ p: null | Map> } interface TerminatedCacheNode { + /** + * Status, represents whether the cached computation returned a value or threw an error. + */ s: 1 + /** + * Value, either the cached result or an error, depending on status. + */ v: T + /** + * Object cache, a `WeakMap` where non-primitive arguments are stored. + */ o: null | WeakMap> + /** + * Primitive cache, a regular `Map` where primitive arguments are stored. + */ p: null | Map> } @@ -24,21 +48,21 @@ type CacheNode = TerminatedCacheNode | UnterminatedCacheNode function createCacheNode(): CacheNode { return { - s: UNTERMINATED, // status, represents whether the cached computation returned a value or threw an error - v: undefined, // value, either the cached result or an error, depending on s - o: null, // object cache, a WeakMap where non-primitive arguments are stored - p: null // primitive cache, a regular Map where primitive arguments are stored. + s: UNTERMINATED, + v: undefined, + o: null, + p: null } } /** * 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 selectors). - * This allows `weakmapMemoize` to have an effectively infinite cache size. + * 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. * - * __Design Tradeoffs for `weakmapMemoize`:__ + * __Design Tradeoffs for `weakMapMemoize`:__ * - Pros: * - 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 `WeakMap`s. @@ -47,7 +71,7 @@ function createCacheNode(): CacheNode { * They're based on strict reference equality. * - It's roughly the same speed as `defaultMemoize`, although likely a fraction slower. * - * __Use Cases for `weakmapMemoize`:__ + * __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 @@ -63,12 +87,12 @@ function createCacheNode(): CacheNode { * ```ts * import { createSelector, weakMapMemoize } from 'reselect' * - * const selectTodoById = createSelector( + * const selectTodosById = createSelector( * [ * (state: RootState) => state.todos, * (state: RootState, id: number) => id * ], - * (todos) => todos[id], + * (todos, id) => todos.filter(todo => todo.id === id), * { memoize: weakMapMemoize } * ) * ``` @@ -78,14 +102,14 @@ function createCacheNode(): CacheNode { * ```ts * import { createSelectorCreator, weakMapMemoize } from 'reselect' * - * const createSelectorWeakmap = createSelectorCreator(weakMapMemoize) + * const createSelectorWeakMap = createSelectorCreator({ memoize: weakMapMemoize, argsMemoize: weakMapMemoize }) * - * const selectTodoById = createSelectorWeakmap( + * const selectTodosById = createSelectorWeakMap( * [ * (state: RootState) => state.todos, * (state: RootState, id: number) => id * ], - * (todos) => todos[id] + * (todos, id) => todos.filter(todo => todo.id === id) * ) * ``` * @@ -96,14 +120,12 @@ function createCacheNode(): CacheNode { * @experimental */ export function weakMapMemoize(func: Func) { - // we reference arguments instead of spreading them for performance reasons - let fnNode = createCacheNode() function memoized() { let cacheNode = fnNode - - for (let i = 0, l = arguments.length; i < l; i++) { + const { length } = arguments + for (let i = 0, l = length; i < l; i++) { const arg = arguments[i] if ( typeof arg === 'function' || @@ -151,5 +173,5 @@ export function weakMapMemoize(func: Func) { fnNode = createCacheNode() } - return memoized as Func & { clearCache: () => void } + return memoized as Func & Simplify } diff --git a/test/autotrackMemoize.spec.ts b/test/autotrackMemoize.spec.ts index 12d4a14ce..bdabb689b 100644 --- a/test/autotrackMemoize.spec.ts +++ b/test/autotrackMemoize.spec.ts @@ -1,7 +1,10 @@ -import { createSelectorCreator, unstable_autotrackMemoize as autotrackMemoize } from 'reselect' +import { + createSelectorCreator, + unstable_autotrackMemoize as autotrackMemoize +} from 'reselect' // Construct 1E6 states for perf test outside of the perf test so as to not change the execute time of the test function -const numOfStates = 1000000 +const numOfStates = 1_000_000 interface StateA { a: number } @@ -32,6 +35,7 @@ describe('Basic selector behavior with autotrack', () => { (state: StateA) => state.a, a => a ) + selector.memoizedResultFunc.clearCache const firstState = { a: 1 } const firstStateNewPointer = { a: 1 } const secondState = { a: 2 } diff --git a/test/benchmarks/createCurriedSelector.bench.ts b/test/benchmarks/createCurriedSelector.bench.ts new file mode 100644 index 000000000..428e6f6d9 --- /dev/null +++ b/test/benchmarks/createCurriedSelector.bench.ts @@ -0,0 +1,53 @@ +import { createCurriedSelector, createSelector } from 'reselect' +import type { Options } from 'tinybench' +import { bench } from 'vitest' +import type { RootState } from '../testUtils' +import { setFunctionNames, setupStore } from '../testUtils' + +describe.only('curriedSelector vs parametric selector', () => { + const options: Options = { + // iterations: 10_000_000, + // time: 0 + } + const store = setupStore() + const state = store.getState() + const parametricSelector = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id) + ) + const curriedSelector = createCurriedSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id) + ) + setFunctionNames({ parametricSelector, curriedSelector }) + bench( + parametricSelector, + () => { + parametricSelector(state, 0) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + parametricSelector.clearCache() + parametricSelector.resetRecomputations() + parametricSelector.memoizedResultFunc.clearCache() + } + } + ) + bench( + curriedSelector, + () => { + curriedSelector(0)(state) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + curriedSelector.clearCache() + curriedSelector.resetRecomputations() + curriedSelector.memoizedResultFunc.clearCache() + } + } + ) +}) diff --git a/test/benchmarks/orderOfExecution.bench.ts b/test/benchmarks/orderOfExecution.bench.ts new file mode 100644 index 000000000..db156ba38 --- /dev/null +++ b/test/benchmarks/orderOfExecution.bench.ts @@ -0,0 +1,117 @@ +import type { Selector } from 'reselect' +import { createSelector } from 'reselect' +import type { Options } from 'tinybench' +import { bench } from 'vitest' +import type { RootState } from '../testUtils' +import { + logRecomputations, + setFunctionNames, + setupStore, + toggleCompleted +} from '../testUtils' + +describe.only('less in input selectors vs more in input selectors', () => { + const store = setupStore() + const state = store.getState() + const arr = Array.from({ length: 1_000_000 }, (e, i) => i) + const runSelector = (selector: Selector) => { + arr.forEach((e, i) => { + selector(store.getState(), 0) + }) + arr.forEach((e, i) => { + selector(store.getState(), 0) + }) + } + + const selectorGood = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id)?.completed + ) + const selectorBad = createSelector( + [ + (state: RootState, id: number) => state.todos.find(todo => todo.id === id) + ], + todo => todo?.completed + ) + + let called = 0 + const nonMemoized = (state: RootState, id: number) => { + called++ + return state.todos.find(todo => todo.id === id)?.completed + } + const options: Options = { + // warmupIterations: 0, + // warmupTime: 0, + iterations: 10, + time: 0 + } + setFunctionNames({ selectorGood, selectorBad, nonMemoized }) + const createOptions = < + S extends Selector & { + recomputations: () => number + dependencyRecomputations: () => number + } + >( + selector: S + ) => { + const options: Options = { + setup: (task, mode) => { + if (mode === 'warmup') return + task.opts = { + beforeEach: () => { + store.dispatch(toggleCompleted(1)) + // store.dispatch(toggleRead(0)) + }, + afterAll: () => { + logRecomputations(selector) + } + } + } + } + return options + } + bench( + selectorGood, + () => { + selectorGood(store.getState(), 0) + }, + { + ...options, + ...createOptions(selectorGood) + } + ) + bench( + selectorBad, + () => { + selectorBad(store.getState(), 0) + }, + { + ...options, + ...createOptions(selectorBad) + } + ) + bench( + nonMemoized, + () => { + nonMemoized(store.getState(), 0) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') { + called = 0 + return + } + task.opts = { + beforeEach: () => { + // store.dispatch(toggleRead(0)) + store.dispatch(toggleCompleted(1)) + }, + afterAll: () => { + console.log(`${nonMemoized.name} called:`, called, `time(s)`) + } + } + } + } + ) +}) diff --git a/test/benchmarks/reselect.bench.ts b/test/benchmarks/reselect.bench.ts new file mode 100644 index 000000000..0b69bd464 --- /dev/null +++ b/test/benchmarks/reselect.bench.ts @@ -0,0 +1,292 @@ +import { + createSelector, + unstable_autotrackMemoize as autotrackMemoize, + weakMapMemoize +} from 'reselect' +import type { Options } from 'tinybench' +import { bench } from 'vitest' +import type { RootState } from '../testUtils' +import { setFunctionNames, setupStore } from '../testUtils' + +const options: Options = { + // iterations: 10_000_000, + // time: 0 +} + +describe.skip('bench', () => { + const store = setupStore() + const state = store.getState() + const selectorDefault = createSelector( + (state: RootState) => state.todos, + todos => todos.map(({ id }) => id) + ) + const selectorAutotrack = createSelector( + (state: RootState) => state.todos, + todos => todos.map(({ id }) => id), + { memoize: autotrackMemoize } + ) + const selectorWeakMap = createSelector( + (state: RootState) => state.todos, + todos => todos.map(({ id }) => id), + { memoize: weakMapMemoize } + ) + const selectorArgsAutotrack = createSelector( + (state: RootState) => state.todos, + todos => todos.map(({ id }) => id), + { argsMemoize: autotrackMemoize } + ) + const nonMemoizedSelector = (state: RootState) => { + return state.todos.map(({ id }) => id) + } + + const selectorArgsWeakMap = createSelector( + (state: RootState) => state.todos, + todos => todos.map(({ id }) => id), + { argsMemoize: weakMapMemoize } + ) + const parametricSelector = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos[id] + ) + const parametricSelectorWeakMapArgs = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos[id], + { argsMemoize: weakMapMemoize } + ) + bench( + 'selectorDefault', + () => { + selectorDefault(state) + }, + options + ) + bench( + 'selectorAutotrack', + () => { + selectorAutotrack(state) + }, + options + ) + bench( + 'selectorWeakMap', + () => { + selectorWeakMap(state) + }, + options + ) + bench( + 'selectorArgsAutotrack', + () => { + selectorArgsAutotrack(state) + }, + options + ) + bench( + 'selectorArgsWeakMap', + () => { + selectorArgsWeakMap(state) + }, + options + ) + bench( + 'non-memoized selector', + () => { + nonMemoizedSelector(state) + }, + options + ) + bench( + 'parametricSelector', + () => { + parametricSelector(state, 0) + }, + options + ) + bench( + 'parametricSelectorWeakMapArgs', + () => { + parametricSelectorWeakMapArgs(state, 0) + }, + options + ) +}) + +describe.skip('for loops', () => { + const store = setupStore() + const state = store.getState() + const { todos } = state + const { length } = todos + bench( + 'for loop length not cached', + () => { + for (let i = 0; i < todos.length; i++) { + // + todos[i].completed + todos[i].id + } + }, + options + ) + bench( + 'for loop length cached', + () => { + for (let i = 0; i < length; i++) { + // + todos[i].completed + todos[i].id + } + }, + options + ) + bench( + 'for loop length and arg cached', + () => { + for (let i = 0; i < length; i++) { + // + const arg = todos[i] + arg.completed + arg.id + } + }, + options + ) +}) + +describe.skip('nested field access', () => { + const store = setupStore() + const state = store.getState() + const selectorDefault = createSelector( + (state: RootState) => state.users, + users => users.user.details.preferences.notifications.push.frequency + ) + const selectorDefault1 = createSelector( + (state: RootState) => state.users.user, + user => user.details.preferences.notifications.push.frequency + ) + const nonMemoizedSelector = (state: RootState) => + state.users.user.details.preferences.notifications.push.frequency + bench( + 'selectorDefault', + () => { + selectorDefault(state) + }, + options + ) + bench( + 'nonMemoizedSelector', + () => { + nonMemoizedSelector(state) + }, + options + ) + bench( + 'selectorDefault1', + () => { + selectorDefault1(state) + }, + options + ) +}) + +describe.skip('simple field access', () => { + const store = setupStore() + const state = store.getState() + const selectorDefault = createSelector( + (state: RootState) => state.users, + users => users.user.details.preferences.notifications.push.frequency + ) + const selectorDefault1 = createSelector( + (state: RootState) => state.users.user, + user => user.details.preferences.notifications.push.frequency + ) + const selectorDefault2 = createSelector( + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + users => users.user.details.preferences.notifications.push.frequency + ) + const nonMemoizedSelector = (state: RootState) => + state.users.user.details.preferences.notifications.push.frequency + bench( + 'selectorDefault', + () => { + selectorDefault(state) + }, + options + ) + bench( + 'nonMemoizedSelector', + () => { + nonMemoizedSelector(state) + }, + options + ) + bench( + 'selectorDefault1', + () => { + selectorDefault1(state) + }, + options + ) + bench( + 'selectorDefault2', + () => { + selectorDefault2(state) + }, + options + ) +}) + +describe.only('field accessors', () => { + const store = setupStore() + const selectorDefault = createSelector( + [(state: RootState) => state.users], + users => users.appSettings + ) + const nonMemoizedSelector = (state: RootState) => state.users.appSettings + + setFunctionNames({ selectorDefault, nonMemoizedSelector }) + + const options: Options = { + // iterations: 1000, + // time: 0 + } + bench( + selectorDefault, + () => { + selectorDefault(store.getState()) + }, + { ...options } + ) + bench( + nonMemoizedSelector, + () => { + nonMemoizedSelector(store.getState()) + }, + { ...options } + ) +}) diff --git a/test/benchmarks/weakMapMemoize.bench.ts b/test/benchmarks/weakMapMemoize.bench.ts new file mode 100644 index 000000000..f3d523635 --- /dev/null +++ b/test/benchmarks/weakMapMemoize.bench.ts @@ -0,0 +1,442 @@ +import type { Selector } from 'reselect' +import { + createSelector, + unstable_autotrackMemoize as autotrackMemoize, + weakMapMemoize +} from 'reselect' +import { bench } from 'vitest' +import type { RootState } from '../testUtils' +import { setFunctionNames, setupStore } from '../testUtils' + +import type { Options } from 'tinybench' + +const store = setupStore() +const state = store.getState() +const arr = Array.from({ length: 30 }, (e, i) => i) + +const options: Options = { + // iterations: 100_000, + // time: 0 +} + +describe.only('weakMapMemoize vs defaultMemoize', () => { + const selectorDefault = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.map(todo => todo.id === id) + ) + const selectorDefaultWithCacheSize = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.map(todo => todo.id === id), + { memoizeOptions: { maxSize: 30 } } + ) + const selectorDefaultWithArgsCacheSize = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.map(todo => todo.id === id), + { argsMemoizeOptions: { maxSize: 30 } } + ) + const selectorDefaultWithBothCacheSize = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.map(todo => todo.id === id), + { memoizeOptions: { maxSize: 30 }, argsMemoizeOptions: { maxSize: 30 } } + ) + const selectorWeakMap = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.map(todo => todo.id === id), + { memoize: weakMapMemoize } + ) + const selectorAutotrack = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.map(todo => todo.id === id), + { memoize: autotrackMemoize } + ) + const selectorArgsAutotrack = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.map(todo => todo.id === id), + { argsMemoize: autotrackMemoize } + ) + const selectorBothAutotrack = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.map(todo => todo.id === id), + { argsMemoize: autotrackMemoize, memoize: autotrackMemoize } + ) + const selectorArgsWeakMap = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.map(todo => todo.id === id), + { argsMemoize: weakMapMemoize } + ) + const selectorBothWeakMap = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos.map(todo => todo.id === id), + { argsMemoize: weakMapMemoize, memoize: weakMapMemoize } + ) + const nonMemoizedSelector = (state: RootState, id: number) => { + return state.todos.map(todo => todo.id === id) + } + setFunctionNames({ + selectorDefault, + selectorDefaultWithCacheSize, + selectorDefaultWithArgsCacheSize, + selectorDefaultWithBothCacheSize, + selectorWeakMap, + selectorArgsWeakMap, + selectorBothWeakMap, + selectorAutotrack, + selectorArgsAutotrack, + selectorBothAutotrack, + nonMemoizedSelector + }) + const runSelector = (selector: Selector) => { + arr.forEach((e, i) => { + selector(state, e) + }) + arr.forEach((e, i) => { + selector(state, e) + }) + } + bench( + selectorDefault, + () => { + runSelector(selectorDefault) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorDefault.clearCache() + selectorDefault.resetRecomputations() + selectorDefault.memoizedResultFunc.clearCache() + task.opts = { + afterAll: () => { + console.log( + `${selectorDefault.name} recomputations after:`, + selectorDefault.recomputations() - 1 + ) + } + } + } + } + ) + bench( + selectorDefaultWithCacheSize, + () => { + runSelector(selectorDefaultWithCacheSize) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorDefaultWithCacheSize.clearCache() + selectorDefaultWithCacheSize.resetRecomputations() + selectorDefaultWithCacheSize.memoizedResultFunc.clearCache() + task.opts = { + afterAll: () => { + console.log( + `${selectorDefaultWithCacheSize.name} recomputations after:`, + selectorDefaultWithCacheSize.recomputations() - 1 + ) + } + } + } + } + ) + bench( + selectorDefaultWithArgsCacheSize, + () => { + runSelector(selectorDefaultWithArgsCacheSize) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorDefaultWithArgsCacheSize.clearCache() + selectorDefaultWithArgsCacheSize.resetRecomputations() + selectorDefaultWithArgsCacheSize.memoizedResultFunc.clearCache() + task.opts = { + afterAll: () => { + console.log( + `${selectorDefaultWithArgsCacheSize.name} recomputations after:`, + selectorDefaultWithArgsCacheSize.recomputations() - 1 + ) + } + } + } + } + ) + bench( + selectorDefaultWithBothCacheSize, + () => { + runSelector(selectorDefaultWithBothCacheSize) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorDefaultWithBothCacheSize.clearCache() + selectorDefaultWithBothCacheSize.resetRecomputations() + selectorDefaultWithBothCacheSize.memoizedResultFunc.clearCache() + task.opts = { + afterAll: () => { + console.log( + `${selectorDefaultWithBothCacheSize.name} recomputations after:`, + selectorDefaultWithBothCacheSize.recomputations() - 1 + ) + } + } + } + } + ) + bench( + selectorWeakMap, + () => { + runSelector(selectorWeakMap) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorWeakMap.clearCache() + selectorWeakMap.resetRecomputations() + selectorWeakMap.memoizedResultFunc.clearCache() + task.opts = { + afterAll: () => { + console.log( + `${selectorWeakMap.name} recomputations after:`, + selectorWeakMap.recomputations() - 1, + selectorWeakMap.dependencyRecomputations() + ) + } + } + } + } + ) + bench( + selectorArgsWeakMap, + () => { + runSelector(selectorArgsWeakMap) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorArgsWeakMap.clearCache() + selectorArgsWeakMap.resetRecomputations() + selectorArgsWeakMap.memoizedResultFunc.clearCache() + task.opts = { + afterAll: () => { + console.log( + `${selectorArgsWeakMap.name} recomputations after:`, + selectorArgsWeakMap.recomputations() - 1, + selectorArgsWeakMap.dependencyRecomputations() + ) + } + } + } + } + ) + bench( + selectorBothWeakMap, + () => { + runSelector(selectorBothWeakMap) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorBothWeakMap.clearCache() + selectorBothWeakMap.resetRecomputations() + selectorBothWeakMap.memoizedResultFunc.clearCache() + task.opts = { + afterAll: () => { + console.log( + `${selectorBothWeakMap.name} recomputations after:`, + selectorBothWeakMap.recomputations() - 1 + ) + } + } + } + } + ) + bench( + selectorAutotrack, + () => { + runSelector(selectorAutotrack) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorAutotrack.clearCache() + selectorAutotrack.resetRecomputations() + selectorAutotrack.memoizedResultFunc.clearCache() + task.opts = { + afterAll: () => { + console.log( + `${selectorAutotrack.name} recomputations after:`, + selectorAutotrack.recomputations() - 1 + ) + } + } + } + } + ) + bench( + selectorArgsAutotrack, + () => { + runSelector(selectorArgsAutotrack) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorArgsAutotrack.clearCache() + selectorArgsAutotrack.resetRecomputations() + selectorArgsAutotrack.memoizedResultFunc.clearCache() + task.opts = { + afterAll: () => { + console.log( + `${selectorArgsAutotrack.name} recomputations after:`, + selectorArgsAutotrack.recomputations() - 1 + ) + } + } + } + } + ) + bench( + selectorBothAutotrack, + () => { + runSelector(selectorBothAutotrack) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorBothAutotrack.clearCache() + selectorBothAutotrack.resetRecomputations() + selectorBothAutotrack.memoizedResultFunc.clearCache() + task.opts = { + afterAll: () => { + console.log( + `${selectorBothAutotrack.name} recomputations after:`, + selectorBothAutotrack.recomputations() - 1 + ) + } + } + } + } + ) + bench( + nonMemoizedSelector, + () => { + runSelector(nonMemoizedSelector) + }, + { ...options } + ) +}) + +describe.skip('weakMapMemoize simple examples', () => { + const selectorDefault = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id) + ) + const selectorWeakMap = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id), + { argsMemoize: weakMapMemoize } + ) + const selectorAutotrack = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id), + { memoize: autotrackMemoize } + ) + + setFunctionNames({ + selectorDefault, + selectorWeakMap, + selectorAutotrack + }) + + bench( + selectorDefault, + () => { + selectorDefault(store.getState()) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorDefault.clearCache() + selectorDefault.resetRecomputations() + selectorDefault.memoizedResultFunc.clearCache() + task.opts = { + // beforeEach: () => { + // store.dispatch(toggleCompleted(0)) + // }, + afterAll: () => { + console.log( + `${selectorDefault.name} recomputations after:`, + selectorDefault.recomputations() - 1 + ) + } + } + } + } + ) + bench( + selectorWeakMap, + () => { + selectorWeakMap(store.getState()) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorWeakMap.clearCache() + selectorWeakMap.resetRecomputations() + selectorWeakMap.memoizedResultFunc.clearCache() + task.opts = { + // beforeEach: () => { + // store.dispatch(toggleCompleted(0)) + // }, + afterAll: () => { + console.log( + `${selectorWeakMap.name} recomputations after:`, + selectorWeakMap.recomputations() - 1 + ) + } + } + } + } + ) + bench( + selectorAutotrack, + () => { + selectorAutotrack(store.getState()) + }, + { + ...options, + setup: (task, mode) => { + if (mode === 'warmup') return + selectorAutotrack.clearCache() + selectorAutotrack.resetRecomputations() + selectorAutotrack.memoizedResultFunc.clearCache() + task.opts = { + // beforeEach: () => { + // store.dispatch(toggleCompleted(0)) + // }, + afterAll: () => { + console.log( + `${selectorAutotrack.name} recomputations after:`, + selectorAutotrack.recomputations() - 1 + ) + } + } + } + } + ) +}) diff --git a/test/reselect.bench.ts b/test/reselect.bench.ts deleted file mode 100644 index 419ee1562..000000000 --- a/test/reselect.bench.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit' -import { bench } from 'vitest' -import { autotrackMemoize } from '../src/autotrackMemoize/autotrackMemoize' -import { weakMapMemoize } from '../src/weakMapMemoize' - -const options: NonNullable[2]> = { - iterations: 1_000_000, - time: 100 -} - -describe('bench', () => { - interface State { - todos: { - id: number - completed: boolean - }[] - } - const state: State = { - todos: [ - { id: 0, completed: false }, - { id: 1, completed: false }, - { id: 2, completed: false }, - { id: 3, completed: false }, - { id: 4, completed: false }, - { id: 5, completed: false }, - { id: 6, completed: false }, - { id: 7, completed: false }, - { id: 8, completed: false }, - { id: 9, completed: false }, - { id: 10, completed: false }, - { id: 11, completed: false }, - { id: 12, completed: false }, - { id: 13, completed: false }, - { id: 14, completed: false }, - { id: 15, completed: false }, - { id: 16, completed: false }, - { id: 17, completed: false }, - { id: 18, completed: false }, - { id: 19, completed: false }, - { id: 20, completed: false }, - { id: 21, completed: false }, - { id: 22, completed: false }, - { id: 23, completed: false }, - { id: 24, completed: false }, - { id: 25, completed: false }, - { id: 26, completed: false }, - { id: 27, completed: false }, - { id: 28, completed: false }, - { id: 29, completed: false }, - { id: 30, completed: false }, - { id: 31, completed: false }, - { id: 32, completed: false }, - { id: 33, completed: false }, - { id: 34, completed: false }, - { id: 35, completed: false }, - { id: 36, completed: false }, - { id: 37, completed: false }, - { id: 38, completed: false }, - { id: 39, completed: false }, - { id: 40, completed: false }, - { id: 41, completed: false }, - { id: 42, completed: false }, - { id: 43, completed: false }, - { id: 44, completed: false }, - { id: 45, completed: false }, - { id: 46, completed: false }, - { id: 47, completed: false }, - { id: 48, completed: false }, - { id: 49, completed: false }, - { id: 50, completed: false }, - { id: 51, completed: false }, - { id: 52, completed: false }, - { id: 53, completed: false }, - { id: 54, completed: false }, - { id: 55, completed: false }, - { id: 56, completed: false }, - { id: 57, completed: false }, - { id: 58, completed: false }, - { id: 59, completed: false }, - { id: 60, completed: false }, - { id: 61, completed: false }, - { id: 62, completed: false }, - { id: 63, completed: false }, - { id: 64, completed: false }, - { id: 65, completed: false }, - { id: 66, completed: false }, - { id: 67, completed: false }, - { id: 68, completed: false }, - { id: 69, completed: false }, - { id: 70, completed: false }, - { id: 71, completed: false }, - { id: 72, completed: false }, - { id: 73, completed: false }, - { id: 74, completed: false }, - { id: 75, completed: false }, - { id: 76, completed: false }, - { id: 77, completed: false }, - { id: 78, completed: false }, - { id: 79, completed: false }, - { id: 80, completed: false }, - { id: 81, completed: false }, - { id: 82, completed: false }, - { id: 83, completed: false }, - { id: 84, completed: false }, - { id: 85, completed: false }, - { id: 86, completed: false }, - { id: 87, completed: false }, - { id: 88, completed: false }, - { id: 89, completed: false }, - { id: 90, completed: false }, - { id: 91, completed: false }, - { id: 92, completed: false }, - { id: 93, completed: false }, - { id: 94, completed: false }, - { id: 95, completed: false }, - { id: 96, completed: false }, - { id: 97, completed: false }, - { id: 98, completed: false }, - { id: 99, completed: false } - ] - } - const selectorDefault = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id) - ) - const selectorAutotrack = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id), - { memoize: autotrackMemoize } - ) - const selectorWeakMap = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id), - { memoize: weakMapMemoize } - ) - const selectorArgsAutotrack = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id), - { argsMemoize: autotrackMemoize } - ) - const nonMemoizedSelector = (state: State) => state.todos.map(t => t.id) - const selectorArgsWeakMap = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id), - { argsMemoize: weakMapMemoize } - ) - const parametricSelector = createSelector( - (state: State) => state.todos, - (state: State, id: number) => id, - (todos, id) => todos[id] - ) - const parametricSelectorWeakMapArgs = createSelector( - (state: State) => state.todos, - (state: State, id: number) => id, - (todos, id) => todos[id], - { - argsMemoize: weakMapMemoize - } - ) - bench( - 'selectorDefault', - () => { - selectorDefault(state) - }, - options - ) - - bench( - 'selectorAutotrack', - () => { - selectorAutotrack(state) - }, - options - ) - bench( - 'selectorWeakMap', - () => { - selectorWeakMap(state) - }, - options - ) - bench( - 'selectorArgsAutotrack', - () => { - selectorArgsAutotrack(state) - }, - options - ) - bench( - 'selectorArgsWeakMap', - () => { - selectorArgsWeakMap(state) - }, - options - ) - bench( - 'non-memoized selector', - () => { - nonMemoizedSelector(state) - }, - options - ) - bench( - 'parametricSelector', - () => { - parametricSelector(state, 0) - }, - options - ) - bench( - 'parametricSelectorWeakMapArgs', - () => { - parametricSelectorWeakMapArgs(state, 0) - }, - options - ) -}) diff --git a/test/reselect.spec.ts b/test/reselect.spec.ts index dfb4abfba..599833640 100644 --- a/test/reselect.spec.ts +++ b/test/reselect.spec.ts @@ -11,11 +11,11 @@ import { } from 'reselect' import type { OutputSelector, OutputSelectorFields } from 'reselect' -import type { LocalTestContext, RootState } from './testUtils' -import { addTodo, deepClone, setupStore, toggleCompleted } from './testUtils' +import type { RootState } from './testUtils' +import { addTodo, deepClone, localTest, toggleCompleted } from './testUtils' // Construct 1E6 states for perf test outside of the perf test so as to not change the execute time of the test function -const numOfStates = 1000000 +const numOfStates = 1_000_000 interface StateA { a: number } @@ -395,49 +395,37 @@ describe('Customizing selectors', () => { expect(memoizer3Calls).toBeGreaterThan(0) }) - test.todo('Test order of execution in a selector', () => { - interface State { - todos: { - id: number - completed: boolean - }[] - } - const state: State = { - todos: [ - { id: 0, completed: false }, - { id: 1, completed: false } - ] - } - // original options untouched. - const selectorOriginal = createSelector( - (state: State) => state.todos, - todos => todos.map(({ id }) => id), - { - inputStabilityCheck: 'always', - memoizeOptions: { - equalityCheck: (a, b) => false, - resultEqualityCheck: (a, b) => false + localTest.todo( + 'Test order of execution in a selector', + ({ store, state }) => { + // original options untouched. + const selectorOriginal = createSelector( + (state: RootState) => state.todos, + todos => todos.map(({ id }) => id), + { + inputStabilityCheck: 'always', + memoizeOptions: { + equalityCheck: (a, b) => false, + resultEqualityCheck: (a, b) => false + } } - } - ) - selectorOriginal(deepClone(state)) - selectorOriginal(deepClone(state)) - const selectorDefaultParametric = createSelector( - [(state: State, id: number) => id, (state: State) => state.todos], - (id, todos) => todos.filter(todo => todo.id === id) - ) - selectorDefaultParametric(state, 1) - selectorDefaultParametric(state, 1) - }) + ) + selectorOriginal(deepClone(state)) + selectorOriginal(deepClone(state)) + const selectorDefaultParametric = createSelector( + [ + (state: RootState, id: number) => id, + (state: RootState) => state.todos + ], + (id, todos) => todos.filter(todo => todo.id === id) + ) + selectorDefaultParametric(state, 1) + selectorDefaultParametric(state, 1) + } + ) }) -describe('argsMemoize and memoize', localTest => { - beforeEach(context => { - const store = setupStore() - context.store = store - context.state = store.getState() - }) - +describe('argsMemoize and memoize', () => { localTest('passing memoize directly to createSelector', ({ store }) => { const state = store.getState() const selectorDefault = createSelector( @@ -470,7 +458,9 @@ describe('argsMemoize and memoize', localTest => { 'lastResult', 'dependencies', 'recomputations', - 'resetRecomputations' + 'resetRecomputations', + 'dependencyRecomputations', + 'resetDependencyRecomputations' ] const memoizerFields: Exclude< keyof OutputSelector, @@ -951,7 +941,9 @@ describe('argsMemoize and memoize', localTest => { 'recomputations', 'resetRecomputations', 'memoize', - 'argsMemoize' + 'argsMemoize', + 'dependencyRecomputations', + 'resetDependencyRecomputations' ]) expect(selectorMicroMemoizeOverrideMemoizeOnly.cache).to.be.an('object') expect(selectorMicroMemoizeOverrideMemoizeOnly.fn).to.be.a('function') @@ -999,22 +991,121 @@ describe('argsMemoize and memoize', localTest => { ).to.be.an('array').that.is.not.empty }) - localTest('pass options object to createSelectorCreator ', ({ store }) => { - const createSelectorMicro = createSelectorCreator({ - memoize: microMemoize, - memoizeOptions: { isEqual: (a, b) => a === b } - }) - const selectorMicro = createSelectorMicro( - [(state: RootState) => state.todos], - todos => todos.map(({ id }) => id) - ) - expect(() => - //@ts-expect-error - createSelectorMicro([(state: RootState) => state.todos], 'a') - ).toThrowError( - TypeError( - `createSelector expects an output function after the inputs, but received: [string]` + localTest( + 'pass options object to createSelectorCreator ', + ({ store, state }) => { + const createSelectorMicro = createSelectorCreator({ + memoize: microMemoize, + memoizeOptions: { isEqual: (a, b) => a === b } + }) + const selectorMicro = createSelectorMicro( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id) ) - ) - }) + const selector = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id) + ) + const selector1 = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id), + { + memoize: weakMapMemoize + } + ) + expect(() => + //@ts-expect-error + createSelectorMicro([(state: RootState) => state.todos], 'a') + ).toThrowError( + TypeError( + `createSelector expects an output function after the inputs, but received: [string]` + ) + ) + const selectorDefault = createSelector( + (state: RootState) => state.users, + users => users.user.details.preferences.notifications.push.frequency + ) + const selectorDefault1 = createSelector( + (state: RootState) => state.users.user, + user => user.details.preferences.notifications.push.frequency + ) + let called = 0 + const selectorDefault2 = createSelector( + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => { + called++ + return state.users + }, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState) => state.users, + (state: RootState, id: number) => state.users, + // (state: RootState) => ({ ...state.users }), + users => { + console.log('run') + return users.user.details.preferences.notifications.push.frequency + }, + { inputStabilityCheck: 'never' } + ) + let start = performance.now() + for (let i = 0; i < 10_000_000; i++) { + selectorDefault(state) + } + console.log(performance.now() - start) + selectorDefault1(state) + const element2 = store.getState() + selectorDefault2(store.getState(), 0) + const element = store.getState() + store.dispatch(toggleCompleted(0)) + const element1 = store.getState() + console.log(element === element1) + console.log(element.alerts === element1.alerts) + console.log(element.todos[1] === element1.todos[1]) + console.log(element === element2) + console.log(element.alerts === element2.alerts) + selectorDefault2(store.getState(), 0) + selectorDefault2(store.getState(), 0) + selectorDefault2(store.getState(), 0) + start = performance.now() + for (let i = 0; i < 100_000_000; i++) { + selector(state) + } + console.log(selector.memoize.name, performance.now() - start) + start = performance.now() + for (let i = 0; i < 100_000_000; i++) { + selector1(state) + } + console.log(selector1.memoize.name, performance.now() - start) + start = performance.now() + for (let i = 0; i < 100; i++) { + selectorDefault2(store.getState(), 0) + // selectorDefault2({ ...state }, 0) + // selectorDefault2({ users: { user: { id: 0, status: '', details: { preferences: { notifications: { push: { frequency: '' } } } } } } }) + } + console.log( + selectorDefault2.memoize.name, + performance.now() - start, + selectorDefault2.recomputations(), + called + ) + } + ) }) diff --git a/test/selectorUtils.spec.ts b/test/selectorUtils.spec.ts index 2e34b4d87..78daee563 100644 --- a/test/selectorUtils.spec.ts +++ b/test/selectorUtils.spec.ts @@ -1,4 +1,5 @@ import { createSelector } from 'reselect' +import type { StateA, StateAB } from 'testTypes' describe('createSelector exposed utils', () => { test('resetRecomputations', () => { diff --git a/test/testUtils.ts b/test/testUtils.ts index c3f6beb78..eb48e6a08 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -1,6 +1,13 @@ import type { PayloadAction } from '@reduxjs/toolkit' import { combineReducers, configureStore, createSlice } from '@reduxjs/toolkit' -import type { AnyFunction, Simplify } from '../src/types' +import { test } from 'vitest' +import type { + AnyFunction, + OutputSelector, + Selector, + SelectorArray, + Simplify +} from '../src/types' interface Todo { id: number @@ -16,6 +23,63 @@ interface Alert { read: boolean } +interface BillingAddress { + street: string + city: string + state: string + zip: string +} + +interface Address extends BillingAddress { + billing: BillingAddress +} + +interface PushNotification { + enabled: boolean + frequency: string +} + +interface Notifications { + email: boolean + sms: boolean + push: PushNotification +} + +interface Preferences { + newsletter: boolean + notifications: Notifications +} + +interface Login { + lastLogin: string + loginCount: number +} + +interface UserDetails { + name: string + email: string + address: Address + preferences: Preferences +} + +interface User { + id: number + details: UserDetails + status: string + login: Login +} + +interface AppSettings { + theme: string + language: string +} + +interface UserState { + user: User + appSettings: AppSettings +} + +// For long arrays const todoState = [ { id: 0, @@ -60,6 +124,29 @@ const todoState = [ completed: false } ] + +export const createTodoItem = (id: number) => { + return { + id, + title: `Task ${id}`, + description: `Description for task ${id}`, + completed: false + } +} + +export const pushToTodos = (howMany: number) => { + const { length: todoStateLength } = todoState + const limit = howMany + todoStateLength + for (let i = todoStateLength; i < limit; i++) { + todoState.push(createTodoItem(i)) + } +} + +pushToTodos(200) + +// for (let i = todoStateLength; i < 200; i++) { +// todoState.push(createTodoItem(i)) +// } const alertState = [ { @@ -103,6 +190,49 @@ const alertState = [ read: false } ] + +// For nested fields tests +const userState: UserState = { + user: { + id: 0, + details: { + name: 'John Doe', + email: 'john.doe@example.com', + address: { + street: '123 Main St', + city: 'AnyTown', + state: 'CA', + zip: '12345', + billing: { + street: '456 Main St', + city: 'AnyTown', + state: 'CA', + zip: '12345' + } + }, + preferences: { + newsletter: true, + notifications: { + email: true, + sms: false, + push: { + enabled: true, + frequency: 'daily' + } + } + } + }, + status: 'active', + login: { + lastLogin: '2023-04-30T12:34:56Z', + loginCount: 123 + } + }, + appSettings: { + theme: 'dark', + language: 'en-US' + } +} const todoSlice = createSlice({ name: 'todos', @@ -151,6 +281,13 @@ const alertSlice = createSlice({ alert.read = true } }, + + toggleRead: (state, action: PayloadAction) => { + const alert = state.find(alert => alert.id === action.payload) + if (alert) { + alert.read = !alert.read + } + }, addAlert: (state, action: PayloadAction>) => { const newId = state.length > 0 ? state[state.length - 1].id + 1 : 0 @@ -165,24 +302,90 @@ const alertSlice = createSlice({ } } }) + +const userSlice = createSlice({ + name: 'users', + initialState: userState, + reducers: { + setUserName: (state, action: PayloadAction) => { + state.user.details.name = action.payload + }, + + setUserEmail: (state, action: PayloadAction) => { + state.user.details.email = action.payload + }, + + setAppTheme: (state, action: PayloadAction) => { + state.appSettings.theme = action.payload + }, + + updateUserStatus: (state, action: PayloadAction) => { + state.user.status = action.payload + }, + + updateLoginDetails: ( + state, + action: PayloadAction<{ lastLogin: string; loginCount: number }> + ) => { + state.user.login = { ...state.user.login, ...action.payload } + }, + + updateUserAddress: (state, action: PayloadAction
) => { + state.user.details.address = { + ...state.user.details.address, + ...action.payload + } + }, + + updateBillingAddress: (state, action: PayloadAction) => { + state.user.details.address.billing = { + ...state.user.details.address.billing, + ...action.payload + } + }, + + toggleNewsletterSubscription: state => { + state.user.details.preferences.newsletter = + !state.user.details.preferences.newsletter + }, + + setNotificationPreferences: ( + state, + action: PayloadAction + ) => { + state.user.details.preferences.notifications = { + ...state.user.details.preferences.notifications, + ...action.payload + } + }, + + updateAppLanguage: (state, action: PayloadAction) => { + state.appSettings.language = action.payload + } + } +}) const rootReducer = combineReducers({ [todoSlice.name]: todoSlice.reducer, - [alertSlice.name]: alertSlice.reducer + [alertSlice.name]: alertSlice.reducer, + [userSlice.name]: userSlice.reducer }) -export const setupStore = () => configureStore({ reducer: rootReducer }) +export const setupStore = (preloadedState?: Partial) => { + return configureStore({ reducer: rootReducer, preloadedState }) +} export type AppStore = Simplify> -export type RootState = Simplify> +export type RootState = ReturnType export interface LocalTestContext { store: AppStore state: RootState } -export const { markAsRead, addAlert, removeAlert } = alertSlice.actions +export const { markAsRead, addAlert, removeAlert, toggleRead } = + alertSlice.actions export const { toggleCompleted, @@ -191,6 +394,8 @@ export const { updateTodo, clearCompleted } = todoSlice.actions + +export const { setUserName, setUserEmail, setAppTheme } = userSlice.actions // Since Node 16 does not support `structuredClone` export const deepClone = (object: T): T => @@ -205,3 +410,37 @@ export const setFunctionNames = (funcObject: Record) => { setFunctionName(value, key) ) } + +const store = setupStore() +const state = store.getState() + +export const localTest = test.extend({ + store, + state +}) + +export const resetSelector = >( + selector: Pick +) => { + selector.clearCache() + selector.resetRecomputations() + selector.memoizedResultFunc.clearCache() +} + +export const logRecomputations = < + S extends Selector & { + recomputations: () => number + dependencyRecomputations: () => number + } +>( + selector: S +) => { + console.log( + `${selector.name} result function recalculated:`, + selector.recomputations(), + `time(s)`, + `input selectors recalculated:`, + selector.dependencyRecomputations(), + `time(s)` + ) +} diff --git a/test/tsconfig.json b/test/tsconfig.json index dc51ba69e..27e1b8705 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -3,12 +3,12 @@ "compilerOptions": { "allowSyntheticDefaultImports": true, "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "node", + "module": "ESNext", + "moduleResolution": "Node", "emitDeclarationOnly": false, "strict": true, "noEmit": true, - "target": "esnext", + "target": "ESNext", "jsx": "react", "baseUrl": ".", "rootDir": ".", @@ -18,8 +18,10 @@ "types": ["vitest/globals"], "paths": { "reselect": ["../src/index.ts"], // @remap-prod-remove-line - "@internal/*": ["src/*"] + "@internal/*": ["../src/*"] } }, - "include": ["**/*.ts"] + "include": [ + "**/*.ts", + ] } diff --git a/test/weakmapMemoize.spec.ts b/test/weakmapMemoize.spec.ts index 369e121ad..b8d91023e 100644 --- a/test/weakmapMemoize.spec.ts +++ b/test/weakmapMemoize.spec.ts @@ -1,7 +1,7 @@ import { createSelectorCreator, weakMapMemoize } from 'reselect' // Construct 1E6 states for perf test outside of the perf test so as to not change the execute time of the test function -const numOfStates = 1000000 +const numOfStates = 1_000_000 interface StateA { a: number } diff --git a/tsconfig.json b/tsconfig.json index daa27554a..388fab465 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "skipLibCheck": true, "allowJs": true, "jsx": "react", + "noErrorTruncation": true, "declaration": true, "emitDeclarationOnly": true, "outDir": "./es", @@ -15,11 +16,11 @@ "experimentalDecorators": true, "rootDirs": ["./src"], "rootDir": "./src", - "types": ["vitest/globals"], + "types": ["vitest/globals"], "baseUrl": ".", "paths": { "reselect": ["src/index.ts"], // @remap-prod-remove-line - "@internal/*": ["src/*"], + "@internal/*": ["src/*"] } }, "include": ["./src/**/*"], diff --git a/type-tests/argsMemoize.test-d.ts b/type-tests/argsMemoize.test-d.ts new file mode 100644 index 000000000..1d58e8ee7 --- /dev/null +++ b/type-tests/argsMemoize.test-d.ts @@ -0,0 +1,894 @@ +import memoizeOne from 'memoize-one' +import microMemoize from 'micro-memoize' +import { + createSelector, + createSelectorCreator, + defaultMemoize, + unstable_autotrackMemoize as autotrackMemoize, + weakMapMemoize +} from 'reselect' +import { assertType, describe, expectTypeOf, test } from 'vitest' + +interface RootState { + todos: { + id: number + completed: boolean + }[] +} + +const state: RootState = { + todos: [ + { id: 0, completed: false }, + { id: 1, completed: false } + ] +} + +describe('memoize and argsMemoize', () => { + test('Override Only Memoize In createSelector', () => { + const selectorDefaultSeparateInlineArgs = createSelector( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { memoize: defaultMemoize } + ) + const selectorDefaultArgsAsArray = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(t => t.id), + { memoize: defaultMemoize } + ) + const selectorDefaultArgsAsArrayWithMemoizeOptions = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(t => t.id), + { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } + ) + const selectorDefaultSeparateInlineArgsWithMemoizeOptions = createSelector( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } + ) + const selectorAutotrackSeparateInlineArgs = createSelector( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { memoize: autotrackMemoize } + ) + const selectorAutotrackArgsAsArray = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(t => t.id), + { memoize: autotrackMemoize } + ) + // @ts-expect-error When memoize is autotrackMemoize, type of memoizeOptions needs to be the same as options args in autotrackMemoize. + const selectorAutotrackArgsAsArrayWithMemoizeOptions = createSelector( + [(state: RootState) => state.todos], + // @ts-expect-error + todos => todos.map(t => t.id), + { memoize: autotrackMemoize, memoizeOptions: { maxSize: 2 } } + ) + const selectorAutotrackSeparateInlineArgsWithMemoizeOptions = + // @ts-expect-error When memoize is autotrackMemoize, type of memoizeOptions needs to be the same as options args in autotrackMemoize. + createSelector( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { memoize: autotrackMemoize, memoizeOptions: { maxSize: 2 } } + ) + const selectorWeakMapSeparateInlineArgs = createSelector( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { memoize: weakMapMemoize } + ) + const selectorWeakMapArgsAsArray = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(t => t.id), + { memoize: weakMapMemoize } + ) + // @ts-expect-error When memoize is weakMapMemoize, type of memoizeOptions needs to be the same as options args in weakMapMemoize. + const selectorWeakMapArgsAsArrayWithMemoizeOptions = createSelector( + [(state: RootState) => state.todos], + // @ts-expect-error + todos => todos.map(t => t.id), + { memoize: weakMapMemoize, memoizeOptions: { maxSize: 2 } } + ) + // @ts-expect-error When memoize is weakMapMemoize, type of memoizeOptions needs to be the same as options args in weakMapMemoize. + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions = createSelector( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { memoize: weakMapMemoize, memoizeOptions: { maxSize: 2 } } + ) + const createSelectorDefault = createSelectorCreator(defaultMemoize) + const createSelectorWeakMap = createSelectorCreator(weakMapMemoize) + const createSelectorAutotrack = createSelectorCreator(autotrackMemoize) + const changeMemoizeMethodSelectorDefault = createSelectorDefault( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { memoize: weakMapMemoize } + ) + const changeMemoizeMethodSelectorWeakMap = createSelectorWeakMap( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { memoize: defaultMemoize } + ) + const changeMemoizeMethodSelectorAutotrack = createSelectorAutotrack( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { memoize: defaultMemoize } + ) + const changeMemoizeMethodSelectorDefaultWithMemoizeOptions = + // @ts-expect-error When memoize is changed to weakMapMemoize or autotrackMemoize, memoizeOptions cannot be the same type as options args in defaultMemoize. + createSelectorDefault( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { memoize: weakMapMemoize, memoizeOptions: { maxSize: 2 } } + ) + const changeMemoizeMethodSelectorWeakMapWithMemoizeOptions = + createSelectorWeakMap( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } // When memoize is changed to defaultMemoize, memoizeOptions can now be the same type as options args in defaultMemoize. + ) + const changeMemoizeMethodSelectorAutotrackWithMemoizeOptions = + createSelectorAutotrack( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } // When memoize is changed to defaultMemoize, memoizeOptions can now be the same type as options args in defaultMemoize. + ) + }) + + test('Override Only argsMemoize In createSelector', () => { + const selectorDefaultSeparateInlineArgs = createSelector( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: defaultMemoize } + ) + const selectorDefaultArgsAsArray = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(t => t.id), + { argsMemoize: defaultMemoize } + ) + const selectorDefaultArgsAsArrayWithMemoizeOptions = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(t => t.id), + { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } + ) + const selectorDefaultSeparateInlineArgsWithMemoizeOptions = createSelector( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } + ) + const selectorAutotrackSeparateInlineArgs = createSelector( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: autotrackMemoize } + ) + const selectorAutotrackArgsAsArray = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(t => t.id), + { argsMemoize: autotrackMemoize } + ) + // @ts-expect-error When argsMemoize is autotrackMemoize, type of argsMemoizeOptions needs to be the same as options args in autotrackMemoize. + const selectorAutotrackArgsAsArrayWithMemoizeOptions = createSelector( + [(state: RootState) => state.todos], + // @ts-expect-error + todos => todos.map(t => t.id), + { + argsMemoize: autotrackMemoize, + argsMemoizeOptions: { maxSize: 2 } + } + ) + const selectorAutotrackSeparateInlineArgsWithMemoizeOptions = + // @ts-expect-error When argsMemoize is autotrackMemoize, type of argsMemoizeOptions needs to be the same as options args in autotrackMemoize. + createSelector( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { + argsMemoize: autotrackMemoize, + argsMemoizeOptions: { maxSize: 2 } + } + ) + const selectorWeakMapSeparateInlineArgs = createSelector( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: weakMapMemoize } + ) + const selectorWeakMapArgsAsArray = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(t => t.id), + { argsMemoize: weakMapMemoize } + ) + // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. + const selectorWeakMapArgsAsArrayWithMemoizeOptions = createSelector( + [(state: RootState) => state.todos], + // @ts-expect-error + todos => todos.map(t => t.id), + { argsMemoize: weakMapMemoize, argsMemoizeOptions: { maxSize: 2 } } + ) + // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions = createSelector( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { argsMemoize: weakMapMemoize, argsMemoizeOptions: { maxSize: 2 } } + ) + // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions1 = createSelector( + [ + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id) + ], + { + argsMemoize: weakMapMemoize, + argsMemoizeOptions: { maxSize: 2 } + } + ) + // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions2 = createSelector( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { + memoize: defaultMemoize, + argsMemoize: weakMapMemoize, + memoizeOptions: { + equalityCheck: + // @ts-expect-error + (a, b) => a === b, + maxSize: 2 + }, + argsMemoizeOptions: { maxSize: 2 } + } + ) + // const createSelectorDefaultMemoize = createSelectorCreator(defaultMemoize) + const createSelectorDefaultMemoize = createSelectorCreator({ + memoize: defaultMemoize + }) + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions3 = + // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. + createSelectorDefaultMemoize( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { + memoize: defaultMemoize, + argsMemoize: weakMapMemoize, + // memoizeOptions: [], + memoizeOptions: [ + { + equalityCheck: + // @ts-expect-error + (a, b) => a === b, + maxSize: 2 + } + ], + argsMemoizeOptions: [{ maxSize: 2 }] + } + ) + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions4 = + // @ts-expect-error + createSelectorDefaultMemoize( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { + memoizeOptions: [{ isPromise: false }], + argsMemoizeOptions: + // @ts-expect-error + (a, b) => a === b + } + ) + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions5 = + // @ts-expect-error + createSelectorDefaultMemoize( + [(state: RootState) => state.todos], + // @ts-expect-error + todos => todos.map(t => t.id), + { + argsMemoize: weakMapMemoize, + memoizeOptions: [{ isPromise: false }], + argsMemoizeOptions: [] + // argsMemoizeOptions: (a, b) => a === b + } + ) + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions6 = + createSelectorDefaultMemoize( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { + argsMemoize: weakMapMemoize, + memoize: weakMapMemoize, + memoizeOptions: [], + argsMemoizeOptions: [] + // argsMemoizeOptions: (a, b) => a === b + } + ) + const createSelectorDefault = createSelectorCreator(defaultMemoize) + const createSelectorWeakMap = createSelectorCreator(weakMapMemoize) + const createSelectorAutotrack = createSelectorCreator(autotrackMemoize) + const changeMemoizeMethodSelectorDefault = createSelectorDefault( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: weakMapMemoize } + ) + const changeMemoizeMethodSelectorWeakMap = createSelectorWeakMap( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: defaultMemoize } + ) + const changeMemoizeMethodSelectorAutotrack = createSelectorAutotrack( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: defaultMemoize } + ) + const changeMemoizeMethodSelectorDefaultWithMemoizeOptions = + // @ts-expect-error When argsMemoize is changed to weakMapMemoize or autotrackMemoize, argsMemoizeOptions cannot be the same type as options args in defaultMemoize. + createSelectorDefault( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { argsMemoize: weakMapMemoize, argsMemoizeOptions: { maxSize: 2 } } + ) + const changeMemoizeMethodSelectorWeakMapWithMemoizeOptions = + createSelectorWeakMap( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } // When argsMemoize is changed to defaultMemoize, argsMemoizeOptions can now be the same type as options args in defaultMemoize. + ) + const changeMemoizeMethodSelectorAutotrackWithMemoizeOptions = + createSelectorAutotrack( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } // When argsMemoize is changed to defaultMemoize, argsMemoizeOptions can now be the same type as options args in defaultMemoize. + ) + }) + + test('Override memoize And argsMemoize In createSelector', () => { + const createSelectorMicroMemoize = createSelectorCreator({ + memoize: microMemoize, + memoizeOptions: [{ isEqual: (a, b) => a === b }], + // memoizeOptions: { isEqual: (a, b) => a === b }, + argsMemoize: microMemoize, + argsMemoizeOptions: { isEqual: (a, b) => a === b } + }) + const selectorMicroMemoize = createSelectorMicroMemoize( + (state: RootState) => state.todos, + todos => todos.map(({ id }) => id) + ) + assertType(selectorMicroMemoize(state)) + // @ts-expect-error + selectorMicroMemoize() + // Checking existence of fields related to `argsMemoize` + selectorMicroMemoize.cache + selectorMicroMemoize.fn() + selectorMicroMemoize.isMemoized + selectorMicroMemoize.options + // @ts-expect-error + selectorMicroMemoize.clearCache() + // Checking existence of fields related to `memoize` + selectorMicroMemoize.memoizedResultFunc.cache + selectorMicroMemoize.memoizedResultFunc.fn() + selectorMicroMemoize.memoizedResultFunc.isMemoized + selectorMicroMemoize.memoizedResultFunc.options + // @ts-expect-error + selectorMicroMemoize.memoizedResultFunc.clearCache() + // Checking existence of fields related to the actual memoized selector + selectorMicroMemoize.dependencies + assertType< + [ + (state: RootState) => { + id: number + completed: boolean + }[] + ] + >(selectorMicroMemoize.dependencies) + assertType(selectorMicroMemoize.lastResult()) + // @ts-expect-error + selectorMicroMemoize.memoizedResultFunc() + assertType( + selectorMicroMemoize.memoizedResultFunc([{ id: 0, completed: true }]) + ) + selectorMicroMemoize.recomputations() + selectorMicroMemoize.resetRecomputations() + // @ts-expect-error + selectorMicroMemoize.resultFunc() + assertType( + selectorMicroMemoize.resultFunc([{ id: 0, completed: true }]) + ) + + // Checking to see if types dynamically change if memoize or argsMemoize are overridden inside `createSelector`. + // `microMemoize` was initially passed into `createSelectorCreator` + // as `memoize` and `argsMemoize`, After overriding them both to `defaultMemoize`, + // not only does the type for `memoizeOptions` and `argsMemoizeOptions` change to + // the options parameter of `defaultMemoize`, the output selector fields + // also change their type to the return type of `defaultMemoize`. + const selectorMicroMemoizeOverridden = createSelectorMicroMemoize( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { + memoize: defaultMemoize, + argsMemoize: defaultMemoize, + memoizeOptions: { equalityCheck: (a, b) => a === b, maxSize: 2 }, + argsMemoizeOptions: { equalityCheck: (a, b) => a === b, maxSize: 3 } + } + ) + assertType(selectorMicroMemoizeOverridden(state)) + // @ts-expect-error + selectorMicroMemoizeOverridden() + // Checking existence of fields related to `argsMemoize` + selectorMicroMemoizeOverridden.clearCache() // Prior to override, this field did NOT exist. + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverridden.cache + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverridden.fn() + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverridden.isMemoized + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverridden.options + // Checking existence of fields related to `memoize` + selectorMicroMemoizeOverridden.memoizedResultFunc.clearCache() // Prior to override, this field did NOT exist. + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverridden.memoizedResultFunc.cache + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverridden.memoizedResultFunc.fn() + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverridden.memoizedResultFunc.isMemoized + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverridden.memoizedResultFunc.options + // Checking existence of fields related to the actual memoized selector + assertType< + [ + (state: RootState) => { + id: number + completed: boolean + }[] + ] + >(selectorMicroMemoizeOverridden.dependencies) + assertType( + selectorMicroMemoizeOverridden.memoizedResultFunc([ + { id: 0, completed: true } + ]) + ) + // @ts-expect-error + selectorMicroMemoizeOverridden.memoizedResultFunc() + selectorMicroMemoizeOverridden.recomputations() + selectorMicroMemoizeOverridden.resetRecomputations() + // @ts-expect-error + selectorMicroMemoizeOverridden.resultFunc() + assertType( + selectorMicroMemoizeOverridden.resultFunc([{ id: 0, completed: true }]) + ) + // Making sure the type behavior is consistent when args are passed in as an array. + const selectorMicroMemoizeOverriddenArray = createSelectorMicroMemoize( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id), + { + memoize: defaultMemoize, + argsMemoize: defaultMemoize, + memoizeOptions: { equalityCheck: (a, b) => a === b, maxSize: 2 }, + argsMemoizeOptions: { equalityCheck: (a, b) => a === b, maxSize: 3 } + } + ) + assertType(selectorMicroMemoizeOverriddenArray(state)) + // @ts-expect-error + selectorMicroMemoizeOverriddenArray() + // Checking existence of fields related to `argsMemoize` + selectorMicroMemoizeOverriddenArray.clearCache() // Prior to override, this field did NOT exist. + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverriddenArray.cache + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverriddenArray.fn() + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverriddenArray.isMemoized + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverriddenArray.options + // Checking existence of fields related to `memoize` + selectorMicroMemoizeOverriddenArray.memoizedResultFunc.clearCache() // Prior to override, this field did NOT exist. + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverriddenArray.memoizedResultFunc.cache + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverriddenArray.memoizedResultFunc.fn() + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverriddenArray.memoizedResultFunc.isMemoized + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverriddenArray.memoizedResultFunc.options + // Checking existence of fields related to the actual memoized selector + assertType< + [ + (state: RootState) => { + id: number + completed: boolean + }[] + ] + >(selectorMicroMemoizeOverriddenArray.dependencies) + assertType( + selectorMicroMemoizeOverriddenArray.memoizedResultFunc([ + { id: 0, completed: true } + ]) + ) + // @ts-expect-error + selectorMicroMemoizeOverriddenArray.memoizedResultFunc() + selectorMicroMemoizeOverriddenArray.recomputations() + selectorMicroMemoizeOverriddenArray.resetRecomputations() + // @ts-expect-error + selectorMicroMemoizeOverriddenArray.resultFunc() + assertType( + selectorMicroMemoizeOverriddenArray.resultFunc([ + { id: 0, completed: true } + ]) + ) + const selectorMicroMemoizeOverrideArgsMemoizeOnlyWrong = + // @ts-expect-error Because `memoizeOptions` should not contain `resultEqualityCheck`. + createSelectorMicroMemoize( + (state: RootState) => state.todos, + todos => todos.map(({ id }) => id), + { + argsMemoize: defaultMemoize, + memoizeOptions: { + isPromise: false, + resultEqualityCheck: + // @ts-expect-error + (a, b) => a === b + }, + argsMemoizeOptions: { resultEqualityCheck: (a, b) => a === b } + } + ) + const selectorMicroMemoizeOverrideArgsMemoizeOnly = + createSelectorMicroMemoize( + (state: RootState) => state.todos, + todos => todos.map(({ id }) => id), + { + argsMemoize: defaultMemoize, + memoizeOptions: { isPromise: false }, + argsMemoizeOptions: { resultEqualityCheck: (a, b) => a === b } + } + ) + assertType(selectorMicroMemoizeOverrideArgsMemoizeOnly(state)) + // @ts-expect-error + selectorMicroMemoizeOverrideArgsMemoizeOnly() + // Checking existence of fields related to `argsMemoize` + selectorMicroMemoizeOverrideArgsMemoizeOnly.clearCache() // Prior to override, this field did NOT exist. + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverrideArgsMemoizeOnly.cache + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverrideArgsMemoizeOnly.fn() + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverrideArgsMemoizeOnly.isMemoized + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverrideArgsMemoizeOnly.options + + // Checking existence of fields related to `memoize`, these should still be the same. + selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc.cache + selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc.fn() + selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc.isMemoized + selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc.options + // @ts-expect-error Note that since we did not override `memoize` in the options object, + // `memoizedResultFunc.clearCache` is still an invalid field access. + selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc.clearCache() + // Checking existence of fields related to the actual memoized selector + assertType< + [ + (state: RootState) => { + id: number + completed: boolean + }[] + ] + >(selectorMicroMemoizeOverrideArgsMemoizeOnly.dependencies) + assertType( + selectorMicroMemoizeOverrideArgsMemoizeOnly.lastResult() + ) + assertType( + selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc([ + { id: 0, completed: true } + ]) + ) + // @ts-expect-error + selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc() + selectorMicroMemoizeOverrideArgsMemoizeOnly.recomputations() + selectorMicroMemoizeOverrideArgsMemoizeOnly.resetRecomputations() + // @ts-expect-error + selectorMicroMemoizeOverrideArgsMemoizeOnly.resultFunc() + assertType( + selectorMicroMemoizeOverrideArgsMemoizeOnly.resultFunc([ + { id: 0, completed: true } + ]) + ) + + const selectorMicroMemoizeOverrideMemoizeOnly = createSelectorMicroMemoize( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { + memoize: defaultMemoize, + memoizeOptions: { resultEqualityCheck: (a, b) => a === b } + } + ) + assertType(selectorMicroMemoizeOverrideMemoizeOnly(state)) + // @ts-expect-error + selectorMicroMemoizeOverrideMemoizeOnly() + + // Checking existence of fields related to `argsMemoize` + selectorMicroMemoizeOverrideMemoizeOnly.cache + selectorMicroMemoizeOverrideMemoizeOnly.fn + selectorMicroMemoizeOverrideMemoizeOnly.isMemoized + selectorMicroMemoizeOverrideMemoizeOnly.options + // @ts-expect-error Note that since we did not override `argsMemoize` in the options object, + // `selector.clearCache` is still an invalid field access. + selectorMicroMemoizeOverrideMemoizeOnly.clearCache() + + // Checking existence of fields related to `memoize` + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc.cache + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc.fn() + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc.isMemoized + // @ts-expect-error Prior to override, this field DID exist. + selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc.options + selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc.clearCache() // Prior to override, this field did NOT exist. + + // Checking existence of fields related to the actual memoized selector + assertType< + [ + (state: RootState) => { + id: number + completed: boolean + }[] + ] + >(selectorMicroMemoizeOverrideMemoizeOnly.dependencies) + assertType(selectorMicroMemoizeOverrideMemoizeOnly.lastResult()) + assertType( + selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc([ + { id: 0, completed: true } + ]) + ) + // @ts-expect-error + selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc() + selectorMicroMemoizeOverrideMemoizeOnly.recomputations() + selectorMicroMemoizeOverrideMemoizeOnly.resetRecomputations() + // @ts-expect-error + selectorMicroMemoizeOverrideMemoizeOnly.resultFunc() + assertType( + selectorMicroMemoizeOverrideMemoizeOnly.resultFunc([ + { id: 0, completed: true } + ]) + ) + + const selectorMicroMemoizePartiallyOverridden = + // @ts-expect-error Since `argsMemoize` is set to `defaultMemoize`, `argsMemoizeOptions` must match the options object parameter of `defaultMemoize` + createSelectorMicroMemoize( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { + memoize: defaultMemoize, + argsMemoize: defaultMemoize, + memoizeOptions: { + equalityCheck: + // @ts-expect-error + (a, b) => a === b, + maxSize: 2 + }, + argsMemoizeOptions: { isPromise: false } // This field causes a type error since it does not match the options param of `defaultMemoize`. + } + ) + const selectorMicroMemoizePartiallyOverridden1 = + // @ts-expect-error Since `argsMemoize` is set to `defaultMemoize`, `argsMemoizeOptions` must match the options object parameter of `defaultMemoize` + createSelectorMicroMemoize( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { + memoize: defaultMemoize, + argsMemoize: defaultMemoize, + memoizeOptions: [ + { + equalityCheck: + // @ts-expect-error + (a, b) => a === b, + maxSize: 2 + } + ], + argsMemoizeOptions: [{ isPromise: false }] // This field causes a type error since it does not match the options param of `defaultMemoize`. + } + ) + const selectorMicroMemoizePartiallyOverridden2 = createSelectorMicroMemoize( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { + // memoizeOptions: [ + // { + // equalityCheck: + // // @ts-expect-error + // (a, b) => a === b, + // maxSize: 2 + // } + // ], + argsMemoizeOptions: [{ isPromise: false }] + } + ) + + const selectorDefaultParametric = createSelector( + (state: RootState, id: number) => id, + (state: RootState) => state.todos, + (id, todos) => todos.filter(todo => todo.id === id), + { + argsMemoize: microMemoize, + inputStabilityCheck: 'never', + memoize: memoizeOne, + argsMemoizeOptions: [], + memoizeOptions: [(a, b) => a === b] + } + ) + assertType< + { + id: number + completed: boolean + }[] + >(selectorDefaultParametric(state, 0)) + assertType< + { + id: number + completed: boolean + }[] + >(selectorDefaultParametric(state, 1)) + // @ts-expect-error + selectorDefaultParametric(state) + // @ts-expect-error + selectorDefaultParametric(1) + // @ts-expect-error + selectorDefaultParametric(state, '') + // @ts-expect-error + selectorDefaultParametric(state, 1, 1) + // Checking existence of fields related to `argsMemoize` + // Prior to override, this field did NOT exist. + selectorDefaultParametric.cache + // Prior to override, this field did NOT exist. + selectorDefaultParametric.fn + // Prior to override, this field did NOT exist. + selectorDefaultParametric.isMemoized + // Prior to override, this field did NOT exist. + selectorDefaultParametric.options + // @ts-expect-error Prior to override, this field DID exist. + selectorDefaultParametric.clearCache() + + // Checking existence of fields related to `memoize` + // @ts-expect-error Prior to override, this field DID exist. + selectorDefaultParametric.memoizedResultFunc.clearCache() + // Prior to override, this field did NOT exist. + selectorDefaultParametric.memoizedResultFunc.clear() + + // Checking existence of fields related to the actual memoized selector + assertType< + [ + (state: RootState, id: number) => number, + (state: RootState) => { id: number; completed: boolean }[] + ] + >(selectorDefaultParametric.dependencies) + assertType<{ id: number; completed: boolean }[]>( + selectorDefaultParametric.lastResult() + ) + assertType<{ id: number; completed: boolean }[]>( + selectorDefaultParametric.memoizedResultFunc(0, [ + { id: 0, completed: true } + ]) + ) + // @ts-expect-error + selectorDefaultParametric.memoizedResultFunc() + selectorDefaultParametric.recomputations() + selectorDefaultParametric.resetRecomputations() + // @ts-expect-error + selectorDefaultParametric.resultFunc() + assertType<{ id: number; completed: boolean }[]>( + selectorDefaultParametric.resultFunc(0, [{ id: 0, completed: true }]) + ) + }) + + test('memoize And argsMemoize In createSelectorCreator', () => { + // If we don't pass in `argsMemoize`, the type for `argsMemoizeOptions` + // falls back to the options parameter of `defaultMemoize`. + const createSelectorArgsMemoizeOptionsFallbackToDefault = + createSelectorCreator({ + memoize: microMemoize, + memoizeOptions: [{ isPromise: false }], + argsMemoizeOptions: { resultEqualityCheck: (a, b) => a === b } + }) + const selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault = + createSelectorArgsMemoizeOptionsFallbackToDefault( + (state: RootState) => state.todos, + todos => todos.map(({ id }) => id) + ) + assertType( + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault(state) + ) + // @ts-expect-error + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault() + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.resultFunc + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.clearCache() + // @ts-expect-error + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.cache + // @ts-expect-error + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.fn + // @ts-expect-error + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.isMemoized + // @ts-expect-error + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.options + // Checking existence of fields related to `memoize` + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc + .cache + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc.fn() + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc + .isMemoized + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc + .options + // @ts-expect-error + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc.clearCache() + // Checking existence of fields related to the actual memoized selector + assertType< + [ + (state: RootState) => { + id: number + completed: boolean + }[] + ] + >(selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.dependencies) + assertType( + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.lastResult() + ) + assertType( + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc( + [{ id: 0, completed: true }] + ) + ) + // @ts-expect-error + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc() + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.recomputations() + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.resetRecomputations() + // @ts-expect-error + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.resultFunc() + assertType( + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.resultFunc([ + { id: 0, completed: true } + ]) + ) + expectTypeOf( + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoize + ).toEqualTypeOf(microMemoize) + expectTypeOf( + selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.argsMemoize + ).toEqualTypeOf(defaultMemoize) + + const createSelectorWithWrongArgsMemoizeOptions = + // @ts-expect-error If we don't pass in `argsMemoize`, the type for `argsMemoizeOptions` falls back to the options parameter of `defaultMemoize`. + createSelectorCreator({ + memoize: microMemoize, + memoizeOptions: { isEqual: (a, b) => a === b }, + argsMemoizeOptions: { + isEqual: + // @ts-expect-error implicit any + (a, b) => a === b + } + }) + + // When passing in an options object as the first argument, there should be no other arguments. + const createSelectorWrong = createSelectorCreator( + { + // @ts-expect-error + memoize: microMemoize, + // @ts-expect-error + memoizeOptions: { isEqual: (a, b) => a === b }, + // @ts-expect-error + argsMemoizeOptions: { equalityCheck: (a, b) => a === b } + }, + [] // This causes the error. + ) + }) + + + test('autotrackMemoize types', () => { + const selector = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(t => t.id), + { memoize: autotrackMemoize } + ) + selector.memoizedResultFunc.clearCache + }) +}) diff --git a/type-tests/createCurriedSrlector.test-d.ts b/type-tests/createCurriedSrlector.test-d.ts new file mode 100644 index 000000000..6611e22c1 --- /dev/null +++ b/type-tests/createCurriedSrlector.test-d.ts @@ -0,0 +1,106 @@ +import { createCurriedSelector, createSelector, defaultMemoize } from 'reselect' +import { describe, expectTypeOf, test } from 'vitest' + +interface RootState { + todos: { + id: number + completed: boolean + }[] +} + +const state: RootState = { + todos: [ + { id: 0, completed: false }, + { id: 1, completed: false } + ] +} + +describe('curried selector', () => { + test('curried selector fields args as array', () => { + const curriedSelector = createCurriedSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos[id] + ) + const parametricSelector = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos[id] + ) + expectTypeOf(curriedSelector.argsMemoize).toEqualTypeOf(defaultMemoize) + expectTypeOf(curriedSelector.memoize).toEqualTypeOf(defaultMemoize) + expectTypeOf(curriedSelector.clearCache).toEqualTypeOf( + parametricSelector.clearCache + ) + expectTypeOf(curriedSelector.dependencies).toEqualTypeOf( + parametricSelector.dependencies + ) + expectTypeOf(curriedSelector.lastResult).toEqualTypeOf( + parametricSelector.lastResult + ) + expectTypeOf(curriedSelector.lastResult).returns.toEqualTypeOf( + parametricSelector.lastResult() + ) + expectTypeOf(curriedSelector.memoizedResultFunc).toEqualTypeOf( + parametricSelector.memoizedResultFunc + ) + expectTypeOf(curriedSelector.recomputations).toEqualTypeOf( + parametricSelector.recomputations + ) + expectTypeOf(curriedSelector.resetRecomputations).toEqualTypeOf( + parametricSelector.resetRecomputations + ) + expectTypeOf(curriedSelector.resultFunc).toEqualTypeOf( + parametricSelector.resultFunc + ) + expectTypeOf(curriedSelector.memoizedResultFunc.clearCache).toEqualTypeOf( + parametricSelector.memoizedResultFunc.clearCache + ) + expectTypeOf(curriedSelector(0)(state)).toEqualTypeOf( + parametricSelector(state, 0) + ) + }) + + test('curried selector fields separate inline args', () => { + const curriedSelector = createCurriedSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos[id] + ) + const parametricSelector = createSelector( + (state: RootState) => state.todos, + (state: RootState, id: number) => id, + (todos, id) => todos[id] + ) + expectTypeOf(curriedSelector.argsMemoize).toEqualTypeOf(defaultMemoize) + expectTypeOf(curriedSelector.memoize).toEqualTypeOf(defaultMemoize) + expectTypeOf(curriedSelector.clearCache).toEqualTypeOf( + parametricSelector.clearCache + ) + expectTypeOf(curriedSelector.dependencies).toEqualTypeOf( + parametricSelector.dependencies + ) + expectTypeOf(curriedSelector.lastResult).toEqualTypeOf( + parametricSelector.lastResult + ) + expectTypeOf(curriedSelector.lastResult).returns.toEqualTypeOf( + parametricSelector.lastResult() + ) + expectTypeOf(curriedSelector.memoizedResultFunc).toEqualTypeOf( + parametricSelector.memoizedResultFunc + ) + expectTypeOf(curriedSelector.recomputations).toEqualTypeOf( + parametricSelector.recomputations + ) + expectTypeOf(curriedSelector.resetRecomputations).toEqualTypeOf( + parametricSelector.resetRecomputations + ) + expectTypeOf(curriedSelector.resultFunc).toEqualTypeOf( + parametricSelector.resultFunc + ) + expectTypeOf(curriedSelector.memoizedResultFunc.clearCache).toEqualTypeOf( + parametricSelector.memoizedResultFunc.clearCache + ) + expectTypeOf(curriedSelector(0)(state)).toEqualTypeOf( + parametricSelector(state, 0) + ) + }) +}) diff --git a/type-tests/deepNesting.test-d.ts b/type-tests/deepNesting.test-d.ts new file mode 100644 index 000000000..5a5e69bc2 --- /dev/null +++ b/type-tests/deepNesting.test-d.ts @@ -0,0 +1,320 @@ +import microMemoize from 'micro-memoize' +import { createSelector, defaultMemoize } from 'reselect' +import { describe, test } from 'vitest' + +interface RootState { + todos: { + id: number + completed: boolean + }[] +} + +const state: RootState = { + todos: [ + { id: 0, completed: false }, + { id: 1, completed: false } + ] +} + +describe('deep nesting', () => { + test('Deep Nesting First And Second createSelector Overload', () => { + type State = { foo: string } + const readOne = (state: State) => state.foo + + const selector0 = createSelector(readOne, one => one) + const selector1 = createSelector(selector0, s => s) + const selector2 = createSelector(selector1, s => s) + const selector3 = createSelector(selector2, s => s) + const selector4 = createSelector(selector3, s => s) + const selector5 = createSelector(selector4, s => s) + const selector6 = createSelector(selector5, s => s) + const selector7 = createSelector(selector6, s => s) + const selector8 = createSelector(selector7, s => s) + const selector9 = createSelector(selector8, s => s) + const selector10 = createSelector(selector9, s => s, { + memoize: microMemoize + }) + selector10.dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].memoizedResultFunc.clearCache + const selector11 = createSelector(selector10, s => s) + const selector12 = createSelector(selector11, s => s) + const selector13 = createSelector(selector12, s => s) + const selector14 = createSelector(selector13, s => s) + const selector15 = createSelector(selector14, s => s) + const selector16 = createSelector(selector15, s => s) + const selector17 = createSelector(selector16, s => s) + const selector18 = createSelector(selector17, s => s) + const selector19 = createSelector(selector18, s => s) + const selector20 = createSelector(selector19, s => s) + selector20.dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].memoizedResultFunc.cache + const selector21 = createSelector(selector20, s => s) + const selector22 = createSelector(selector21, s => s) + const selector23 = createSelector(selector22, s => s) + const selector24 = createSelector(selector23, s => s) + const selector25 = createSelector(selector24, s => s) + const selector26 = createSelector(selector25, s => s) + const selector27 = createSelector(selector26, s => s) + const selector28 = createSelector(selector27, s => s) + const selector29 = createSelector(selector28, s => s) + const selector30 = createSelector(selector29, s => s) + selector30.dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].dependencies[0].dependencies[0] + .dependencies[0].dependencies[0].memoizedResultFunc.clearCache + }) + test('Deep Nesting Second createSelector Overload', () => { + type State = { foo: string } + const readOne = (state: State) => state.foo + + const selector0 = createSelector(readOne, one => one) + const selector1 = createSelector(selector0, s => s, { + memoize: defaultMemoize + }) + const selector2 = createSelector(selector1, s => s, { + memoize: defaultMemoize + }) + const selector3 = createSelector(selector2, s => s, { + memoize: defaultMemoize + }) + const selector4 = createSelector(selector3, s => s, { + memoize: defaultMemoize + }) + const selector5 = createSelector(selector4, s => s, { + memoize: defaultMemoize + }) + const selector6 = createSelector(selector5, s => s, { + memoize: defaultMemoize + }) + const selector7 = createSelector(selector6, s => s, { + memoize: defaultMemoize + }) + const selector8 = createSelector(selector7, s => s, { + memoize: defaultMemoize + }) + const selector9 = createSelector(selector8, s => s, { + memoize: defaultMemoize + }) + const selector10 = createSelector(selector9, s => s, { + memoize: defaultMemoize + }) + const selector11 = createSelector(selector10, s => s, { + memoize: defaultMemoize + }) + const selector12 = createSelector(selector11, s => s, { + memoize: defaultMemoize + }) + const selector13 = createSelector(selector12, s => s, { + memoize: defaultMemoize + }) + const selector14 = createSelector(selector13, s => s, { + memoize: defaultMemoize + }) + const selector15 = createSelector(selector14, s => s, { + memoize: defaultMemoize + }) + const selector16 = createSelector(selector15, s => s, { + memoize: defaultMemoize + }) + const selector17 = createSelector(selector16, s => s, { + memoize: defaultMemoize + }) + const selector18 = createSelector(selector17, s => s, { + memoize: defaultMemoize + }) + const selector19 = createSelector(selector18, s => s, { + memoize: defaultMemoize + }) + const selector20 = createSelector(selector19, s => s, { + memoize: defaultMemoize + }) + const selector21 = createSelector(selector20, s => s, { + memoize: defaultMemoize + }) + const selector22 = createSelector(selector21, s => s, { + memoize: defaultMemoize + }) + const selector23 = createSelector(selector22, s => s, { + memoize: defaultMemoize + }) + const selector24 = createSelector(selector23, s => s, { + memoize: defaultMemoize + }) + const selector25 = createSelector(selector24, s => s, { + memoize: defaultMemoize + }) + const selector26 = createSelector(selector25, s => s, { + memoize: defaultMemoize + }) + const selector27 = createSelector(selector26, s => s, { + memoize: defaultMemoize + }) + const selector28 = createSelector(selector27, s => s, { + memoize: defaultMemoize + }) + const selector29 = createSelector(selector28, s => s, { + memoize: defaultMemoize + }) + }) + + test('Deep Nesting Third createSelector Overload', () => { + type State = { foo: string } + const readOne = (state: State) => state.foo + + const selector0 = createSelector(readOne, one => one) + const selector1 = createSelector([selector0], s => s) + const selector2 = createSelector([selector1], s => s) + const selector3 = createSelector([selector2], s => s) + const selector4 = createSelector([selector3], s => s) + const selector5 = createSelector([selector4], s => s) + const selector6 = createSelector([selector5], s => s) + const selector7 = createSelector([selector6], s => s) + const selector8 = createSelector([selector7], s => s) + const selector9 = createSelector([selector8], s => s) + const selector10 = createSelector([selector9], s => s) + const selector11 = createSelector([selector10], s => s) + const selector12 = createSelector([selector11], s => s) + const selector13 = createSelector([selector12], s => s) + const selector14 = createSelector([selector13], s => s) + const selector15 = createSelector([selector14], s => s) + const selector16 = createSelector([selector15], s => s) + const selector17 = createSelector([selector16], s => s) + const selector18 = createSelector([selector17], s => s) + const selector19 = createSelector([selector18], s => s) + const selector20 = createSelector([selector19], s => s) + const selector21 = createSelector([selector20], s => s) + const selector22 = createSelector([selector21], s => s) + const selector23 = createSelector([selector22], s => s) + const selector24 = createSelector([selector23], s => s) + const selector25 = createSelector([selector24], s => s) + const selector26 = createSelector([selector25], s => s) + const selector27 = createSelector([selector26], s => s) + const selector28 = createSelector([selector27], s => s) + const selector29 = createSelector([selector28], s => s) + const selector30 = createSelector([selector29], s => s) + }) + + test('createSelector Parameter Limit', () => { + const selector = createSelector( + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testBoolean: boolean }) => state.testBoolean, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testStringArray: string[] }) => state.testStringArray, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testBoolean: boolean }) => state.testBoolean, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testStringArray: string[] }) => state.testStringArray, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testBoolean: boolean }) => state.testBoolean, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testStringArray: string[] }) => state.testStringArray, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testBoolean: boolean }) => state.testBoolean, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testString: string }) => state.testString, + (state: { testNumber: number }) => state.testNumber, + (state: { testStringArray: string[] }) => state.testStringArray, + ( + foo1: string, + foo2: number, + foo3: boolean, + foo4: string, + foo5: string, + foo6: string, + foo7: string, + foo8: number, + foo9: string[], + foo10: string, + foo11: number, + foo12: boolean, + foo13: string, + foo14: string, + foo15: string, + foo16: string, + foo17: number, + foo18: string[], + foo19: string, + foo20: number, + foo21: boolean, + foo22: string, + foo23: string, + foo24: string, + foo25: string, + foo26: number, + foo27: string[], + foo28: string, + foo29: number, + foo30: boolean, + foo31: string, + foo32: string, + foo33: string, + foo34: string, + foo35: number, + foo36: string[] + ) => { + return { + foo1, + foo2, + foo3, + foo4, + foo5, + foo6, + foo7, + foo8, + foo9, + foo10, + foo11, + foo12, + foo13, + foo14, + foo15, + foo16, + foo17, + foo18, + foo19, + foo20, + foo21, + foo22, + foo23, + foo24, + foo25, + foo26, + foo27, + foo28, + foo29, + foo30, + foo31, + foo32, + foo33, + foo34, + foo35, + foo36 + } + } + ) + }) +}) diff --git a/type-tests/tsconfig.json b/type-tests/tsconfig.json new file mode 100644 index 000000000..7c516c1b4 --- /dev/null +++ b/type-tests/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "commonjs", + "strict": true, + "target": "ES2015", + "declaration": true, + "noEmit": true, + "skipLibCheck": true, + "paths": { + "reselect": ["../src/index"], // @remap-prod-remove-line + "@internal/*": ["../src/*"] + } + } +} diff --git a/typescript_test/argsMemoize.typetest.ts b/typescript_test/argsMemoize.typetest.ts index 649debf4e..2093b8920 100644 --- a/typescript_test/argsMemoize.typetest.ts +++ b/typescript_test/argsMemoize.typetest.ts @@ -7,15 +7,15 @@ import { unstable_autotrackMemoize as autotrackMemoize, weakMapMemoize } from 'reselect' -import { expectExactType } from './test' +import { expectExactType } from './typesTestUtils' -interface State { +interface RootState { todos: { id: number completed: boolean }[] } -const state: State = { +const state: RootState = { todos: [ { id: 0, completed: false }, { id: 1, completed: false } @@ -24,69 +24,69 @@ const state: State = { function overrideOnlyMemoizeInCreateSelector() { const selectorDefaultSeparateInlineArgs = createSelector( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize } ) const selectorDefaultArgsAsArray = createSelector( - [(state: State) => state.todos], + [(state: RootState) => state.todos], todos => todos.map(t => t.id), { memoize: defaultMemoize } ) const selectorDefaultArgsAsArrayWithMemoizeOptions = createSelector( - [(state: State) => state.todos], + [(state: RootState) => state.todos], todos => todos.map(t => t.id), { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } ) const selectorDefaultSeparateInlineArgsWithMemoizeOptions = createSelector( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } ) const selectorAutotrackSeparateInlineArgs = createSelector( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { memoize: autotrackMemoize } ) const selectorAutotrackArgsAsArray = createSelector( - [(state: State) => state.todos], + [(state: RootState) => state.todos], todos => todos.map(t => t.id), { memoize: autotrackMemoize } ) // @ts-expect-error When memoize is autotrackMemoize, type of memoizeOptions needs to be the same as options args in autotrackMemoize. const selectorAutotrackArgsAsArrayWithMemoizeOptions = createSelector( - [(state: State) => state.todos], + [(state: RootState) => state.todos], // @ts-expect-error todos => todos.map(t => t.id), { memoize: autotrackMemoize, memoizeOptions: { maxSize: 2 } } ) // @ts-expect-error When memoize is autotrackMemoize, type of memoizeOptions needs to be the same as options args in autotrackMemoize. const selectorAutotrackSeparateInlineArgsWithMemoizeOptions = createSelector( - (state: State) => state.todos, + (state: RootState) => state.todos, // @ts-expect-error todos => todos.map(t => t.id), { memoize: autotrackMemoize, memoizeOptions: { maxSize: 2 } } ) const selectorWeakMapSeparateInlineArgs = createSelector( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { memoize: weakMapMemoize } ) const selectorWeakMapArgsAsArray = createSelector( - [(state: State) => state.todos], + [(state: RootState) => state.todos], todos => todos.map(t => t.id), { memoize: weakMapMemoize } ) // @ts-expect-error When memoize is weakMapMemoize, type of memoizeOptions needs to be the same as options args in weakMapMemoize. const selectorWeakMapArgsAsArrayWithMemoizeOptions = createSelector( - [(state: State) => state.todos], + [(state: RootState) => state.todos], // @ts-expect-error todos => todos.map(t => t.id), { memoize: weakMapMemoize, memoizeOptions: { maxSize: 2 } } ) // @ts-expect-error When memoize is weakMapMemoize, type of memoizeOptions needs to be the same as options args in weakMapMemoize. const selectorWeakMapSeparateInlineArgsWithMemoizeOptions = createSelector( - (state: State) => state.todos, + (state: RootState) => state.todos, // @ts-expect-error todos => todos.map(t => t.id), { memoize: weakMapMemoize, memoizeOptions: { maxSize: 2 } } @@ -95,37 +95,37 @@ function overrideOnlyMemoizeInCreateSelector() { const createSelectorWeakMap = createSelectorCreator(weakMapMemoize) const createSelectorAutotrack = createSelectorCreator(autotrackMemoize) const changeMemoizeMethodSelectorDefault = createSelectorDefault( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { memoize: weakMapMemoize } ) const changeMemoizeMethodSelectorWeakMap = createSelectorWeakMap( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize } ) const changeMemoizeMethodSelectorAutotrack = createSelectorAutotrack( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize } ) const changeMemoizeMethodSelectorDefaultWithMemoizeOptions = // @ts-expect-error When memoize is changed to weakMapMemoize or autotrackMemoize, memoizeOptions cannot be the same type as options args in defaultMemoize. createSelectorDefault( - (state: State) => state.todos, + (state: RootState) => state.todos, // @ts-expect-error todos => todos.map(t => t.id), { memoize: weakMapMemoize, memoizeOptions: { maxSize: 2 } } ) const changeMemoizeMethodSelectorWeakMapWithMemoizeOptions = createSelectorWeakMap( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } // When memoize is changed to defaultMemoize, memoizeOptions can now be the same type as options args in defaultMemoize. ) const changeMemoizeMethodSelectorAutotrackWithMemoizeOptions = createSelectorAutotrack( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } // When memoize is changed to defaultMemoize, memoizeOptions can now be the same type as options args in defaultMemoize. ) @@ -133,38 +133,38 @@ function overrideOnlyMemoizeInCreateSelector() { function overrideOnlyArgsMemoizeInCreateSelector() { const selectorDefaultSeparateInlineArgs = createSelector( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { argsMemoize: defaultMemoize } ) const selectorDefaultArgsAsArray = createSelector( - [(state: State) => state.todos], + [(state: RootState) => state.todos], todos => todos.map(t => t.id), { argsMemoize: defaultMemoize } ) const selectorDefaultArgsAsArrayWithMemoizeOptions = createSelector( - [(state: State) => state.todos], + [(state: RootState) => state.todos], todos => todos.map(t => t.id), { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } ) const selectorDefaultSeparateInlineArgsWithMemoizeOptions = createSelector( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } ) const selectorAutotrackSeparateInlineArgs = createSelector( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { argsMemoize: autotrackMemoize } ) const selectorAutotrackArgsAsArray = createSelector( - [(state: State) => state.todos], + [(state: RootState) => state.todos], todos => todos.map(t => t.id), { argsMemoize: autotrackMemoize } ) // @ts-expect-error When argsMemoize is autotrackMemoize, type of argsMemoizeOptions needs to be the same as options args in autotrackMemoize. const selectorAutotrackArgsAsArrayWithMemoizeOptions = createSelector( - [(state: State) => state.todos], + [(state: RootState) => state.todos], // @ts-expect-error todos => todos.map(t => t.id), { @@ -174,7 +174,7 @@ function overrideOnlyArgsMemoizeInCreateSelector() { ) // @ts-expect-error When argsMemoize is autotrackMemoize, type of argsMemoizeOptions needs to be the same as options args in autotrackMemoize. const selectorAutotrackSeparateInlineArgsWithMemoizeOptions = createSelector( - (state: State) => state.todos, + (state: RootState) => state.todos, // @ts-expect-error todos => todos.map(t => t.id), { @@ -183,64 +183,156 @@ function overrideOnlyArgsMemoizeInCreateSelector() { } ) const selectorWeakMapSeparateInlineArgs = createSelector( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { argsMemoize: weakMapMemoize } ) const selectorWeakMapArgsAsArray = createSelector( - [(state: State) => state.todos], + [(state: RootState) => state.todos], todos => todos.map(t => t.id), { argsMemoize: weakMapMemoize } ) // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. const selectorWeakMapArgsAsArrayWithMemoizeOptions = createSelector( - [(state: State) => state.todos], + [(state: RootState) => state.todos], // @ts-expect-error todos => todos.map(t => t.id), { argsMemoize: weakMapMemoize, argsMemoizeOptions: { maxSize: 2 } } ) // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. const selectorWeakMapSeparateInlineArgsWithMemoizeOptions = createSelector( - (state: State) => state.todos, + (state: RootState) => state.todos, // @ts-expect-error todos => todos.map(t => t.id), { argsMemoize: weakMapMemoize, argsMemoizeOptions: { maxSize: 2 } } ) + // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions1 = createSelector( + [ + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id) + ], + { + argsMemoize: weakMapMemoize, + argsMemoizeOptions: { maxSize: 2 } + } + ) + // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions2 = createSelector( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { + memoize: defaultMemoize, + argsMemoize: weakMapMemoize, + memoizeOptions: { + equalityCheck: + // @ts-expect-error + (a, b) => a === b, + maxSize: 2 + }, + argsMemoizeOptions: { maxSize: 2 } + } + ) + // const createSelectorDefaultMemoize = createSelectorCreator(defaultMemoize) + const createSelectorDefaultMemoize = createSelectorCreator({ + memoize: defaultMemoize + }) + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions3 = + // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. + createSelectorDefaultMemoize( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { + memoize: defaultMemoize, + argsMemoize: weakMapMemoize, + // memoizeOptions: [], + memoizeOptions: [ + { + equalityCheck: + // @ts-expect-error + (a, b) => a === b, + maxSize: 2 + } + ], + argsMemoizeOptions: [{ maxSize: 2 }] + } + ) + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions4 = + // @ts-expect-error + createSelectorDefaultMemoize( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { + memoizeOptions: [{ isPromise: false }], + argsMemoizeOptions: + // @ts-expect-error + (a, b) => a === b + } + ) + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions5 = + // @ts-expect-error + createSelectorDefaultMemoize( + [(state: RootState) => state.todos], + // @ts-expect-error + todos => todos.map(t => t.id), + { + argsMemoize: weakMapMemoize, + memoizeOptions: [{ isPromise: false }], + argsMemoizeOptions: [] + // argsMemoizeOptions: (a, b) => a === b + } + ) + const selectorWeakMapSeparateInlineArgsWithMemoizeOptions6 = + createSelectorDefaultMemoize( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { + argsMemoize: weakMapMemoize, + memoize: weakMapMemoize, + memoizeOptions: [], + argsMemoizeOptions: [] + // argsMemoizeOptions: (a, b) => a === b + } + ) const createSelectorDefault = createSelectorCreator(defaultMemoize) const createSelectorWeakMap = createSelectorCreator(weakMapMemoize) const createSelectorAutotrack = createSelectorCreator(autotrackMemoize) const changeMemoizeMethodSelectorDefault = createSelectorDefault( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { argsMemoize: weakMapMemoize } ) const changeMemoizeMethodSelectorWeakMap = createSelectorWeakMap( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { argsMemoize: defaultMemoize } ) const changeMemoizeMethodSelectorAutotrack = createSelectorAutotrack( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { argsMemoize: defaultMemoize } ) const changeMemoizeMethodSelectorDefaultWithMemoizeOptions = // @ts-expect-error When argsMemoize is changed to weakMapMemoize or autotrackMemoize, argsMemoizeOptions cannot be the same type as options args in defaultMemoize. createSelectorDefault( - (state: State) => state.todos, + (state: RootState) => state.todos, // @ts-expect-error todos => todos.map(t => t.id), { argsMemoize: weakMapMemoize, argsMemoizeOptions: { maxSize: 2 } } ) const changeMemoizeMethodSelectorWeakMapWithMemoizeOptions = createSelectorWeakMap( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } // When argsMemoize is changed to defaultMemoize, argsMemoizeOptions can now be the same type as options args in defaultMemoize. ) const changeMemoizeMethodSelectorAutotrackWithMemoizeOptions = createSelectorAutotrack( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } // When argsMemoize is changed to defaultMemoize, argsMemoizeOptions can now be the same type as options args in defaultMemoize. ) @@ -249,12 +341,13 @@ function overrideOnlyArgsMemoizeInCreateSelector() { function overrideMemoizeAndArgsMemoizeInCreateSelector() { const createSelectorMicroMemoize = createSelectorCreator({ memoize: microMemoize, - memoizeOptions: { isEqual: (a, b) => a === b }, + memoizeOptions: [{ isEqual: (a, b) => a === b }], + // memoizeOptions: { isEqual: (a, b) => a === b }, argsMemoize: microMemoize, argsMemoizeOptions: { isEqual: (a, b) => a === b } }) const selectorMicroMemoize = createSelectorMicroMemoize( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(({ id }) => id) ) expectExactType(selectorMicroMemoize(state)) @@ -278,7 +371,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { selectorMicroMemoize.dependencies expectExactType< [ - (state: State) => { + (state: RootState) => { id: number completed: boolean }[] @@ -305,7 +398,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { // the options parameter of `defaultMemoize`, the output selector fields // also change their type to the return type of `defaultMemoize`. const selectorMicroMemoizeOverridden = createSelectorMicroMemoize( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize, @@ -340,7 +433,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { // Checking existence of fields related to the actual memoized selector expectExactType< [ - (state: State) => { + (state: RootState) => { id: number completed: boolean }[] @@ -362,7 +455,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { ) // Making sure the type behavior is consistent when args are passed in as an array. const selectorMicroMemoizeOverriddenArray = createSelectorMicroMemoize( - [(state: State) => state.todos], + [(state: RootState) => state.todos], todos => todos.map(({ id }) => id), { memoize: defaultMemoize, @@ -397,7 +490,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { // Checking existence of fields related to the actual memoized selector expectExactType< [ - (state: State) => { + (state: RootState) => { id: number completed: boolean }[] @@ -420,20 +513,22 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { const selectorMicroMemoizeOverrideArgsMemoizeOnlyWrong = // @ts-expect-error Because `memoizeOptions` should not contain `resultEqualityCheck`. createSelectorMicroMemoize( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(({ id }) => id), { argsMemoize: defaultMemoize, memoizeOptions: { isPromise: false, - resultEqualityCheck: (a: unknown, b: unknown) => a === b + resultEqualityCheck: + // @ts-expect-error + (a, b) => a === b }, argsMemoizeOptions: { resultEqualityCheck: (a, b) => a === b } } ) const selectorMicroMemoizeOverrideArgsMemoizeOnly = createSelectorMicroMemoize( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(({ id }) => id), { argsMemoize: defaultMemoize, @@ -466,7 +561,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { // Checking existence of fields related to the actual memoized selector expectExactType< [ - (state: State) => { + (state: RootState) => { id: number completed: boolean }[] @@ -493,7 +588,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { ) const selectorMicroMemoizeOverrideMemoizeOnly = createSelectorMicroMemoize( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize, @@ -527,7 +622,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { // Checking existence of fields related to the actual memoized selector expectExactType< [ - (state: State) => { + (state: RootState) => { id: number completed: boolean }[] @@ -553,32 +648,70 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { ]) ) - // @ts-expect-error Since `argsMemoize` is set to `defaultMemoize`, - // `argsMemoizeOptions` must match the options object parameter of `defaultMemoize` - const selectorMicroMemoizePartiallyOverridden = createSelectorMicroMemoize( - (state: State) => state.todos, - // @ts-expect-error - todos => todos.map(t => t.id), - { - memoize: defaultMemoize, - argsMemoize: defaultMemoize, - memoizeOptions: { - // @ts-expect-error - equalityCheck: (a, b) => a === b, - maxSize: 2 - }, - argsMemoizeOptions: { isPromise: false } // This field causes a type error since it does not match the options param of `defaultMemoize`. - } - ) + const selectorMicroMemoizePartiallyOverridden = + // @ts-expect-error Since `argsMemoize` is set to `defaultMemoize`, `argsMemoizeOptions` must match the options object parameter of `defaultMemoize` + createSelectorMicroMemoize( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { + memoize: defaultMemoize, + argsMemoize: defaultMemoize, + memoizeOptions: { + equalityCheck: + // @ts-expect-error + (a, b) => a === b, + maxSize: 2 + }, + argsMemoizeOptions: { isPromise: false } // This field causes a type error since it does not match the options param of `defaultMemoize`. + } + ) + const selectorMicroMemoizePartiallyOverridden1 = + // @ts-expect-error Since `argsMemoize` is set to `defaultMemoize`, `argsMemoizeOptions` must match the options object parameter of `defaultMemoize` + createSelectorMicroMemoize( + (state: RootState) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { + memoize: defaultMemoize, + argsMemoize: defaultMemoize, + memoizeOptions: [ + { + equalityCheck: + // @ts-expect-error + (a, b) => a === b, + maxSize: 2 + } + ], + argsMemoizeOptions: [{ isPromise: false }] // This field causes a type error since it does not match the options param of `defaultMemoize`. + } + ) + const selectorMicroMemoizePartiallyOverridden2 = + createSelectorMicroMemoize( + (state: RootState) => state.todos, + todos => todos.map(t => t.id), + { + // memoizeOptions: [ + // { + // equalityCheck: + // // @ts-expect-error + // (a, b) => a === b, + // maxSize: 2 + // } + // ], + argsMemoizeOptions: [{ isPromise: false }] + } + ) const selectorDefaultParametric = createSelector( - (state: State, id: number) => id, - (state: State) => state.todos, + (state: RootState, id: number) => id, + (state: RootState) => state.todos, (id, todos) => todos.filter(todo => todo.id === id), { argsMemoize: microMemoize, inputStabilityCheck: 'never', memoize: memoizeOne, + argsMemoizeOptions: [], memoizeOptions: [(a, b) => a === b] } ) @@ -623,8 +756,8 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { // Checking existence of fields related to the actual memoized selector expectExactType< [ - (state: State, id: number) => number, - (state: State) => { id: number; completed: boolean }[] + (state: RootState, id: number) => number, + (state: RootState) => { id: number; completed: boolean }[] ] >(selectorDefaultParametric.dependencies) expectExactType<{ id: number; completed: boolean }[]>( @@ -657,7 +790,7 @@ function memoizeAndArgsMemoizeInCreateSelectorCreator() { }) const selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault = createSelectorArgsMemoizeOptionsFallbackToDefault( - (state: State) => state.todos, + (state: RootState) => state.todos, todos => todos.map(({ id }) => id) ) expectExactType( @@ -688,7 +821,7 @@ function memoizeAndArgsMemoizeInCreateSelectorCreator() { // Checking existence of fields related to the actual memoized selector expectExactType< [ - (state: State) => { + (state: RootState) => { id: number completed: boolean }[] @@ -725,8 +858,11 @@ function memoizeAndArgsMemoizeInCreateSelectorCreator() { createSelectorCreator({ memoize: microMemoize, memoizeOptions: { isEqual: (a, b) => a === b }, - // @ts-expect-error - argsMemoizeOptions: { isEqual: (a, b) => a === b } + argsMemoizeOptions: { + isEqual: + // @ts-expect-error implicit any + (a, b) => a === b + } }) // When passing in an options object as the first argument, there should be no other arguments. diff --git a/typescript_test/test.ts b/typescript_test/test.ts index c6be7e511..379768f60 100644 --- a/typescript_test/test.ts +++ b/typescript_test/test.ts @@ -20,10 +20,7 @@ import { defaultEqualityCheck, defaultMemoize } from 'reselect' - -export function expectType(t: T): T { - return t -} +import { expectExactType } from './typesTestUtils' type Exact = (() => T extends A ? 1 : 0) extends () => T extends B ? 1 @@ -35,25 +32,6 @@ type Exact = (() => T extends A ? 1 : 0) extends () => T extends B : never : never -export declare type IsAny = true | false extends ( - T extends never ? true : false -) - ? True - : False - -export declare type IsUnknown = unknown extends T - ? IsAny - : False - -type Equals = IsAny< - T, - never, - IsAny -> -export function expectExactType(t: T) { - return >(u: U) => {} -} - interface StateA { a: number } diff --git a/typescript_test/typesTestUtils.ts b/typescript_test/typesTestUtils.ts new file mode 100644 index 000000000..90d0d5b23 --- /dev/null +++ b/typescript_test/typesTestUtils.ts @@ -0,0 +1,23 @@ +export function expectType(t: T): T { + return t +} + +export declare type IsAny = true | false extends ( + T extends never ? true : false +) + ? True + : False + +export declare type IsUnknown = unknown extends T + ? IsAny + : False + +type Equals = IsAny< + T, + never, + IsAny +> + +export function expectExactType(t: T) { + return >(u: U) => {} +} diff --git a/vitest.config.ts b/vitest.config.ts index 0ee99ef30..dad9aad5b 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { + typecheck: { tsconfig: './type-tests/tsconfig.json' }, globals: true, include: ['./test/**/*.(spec|test).[jt]s?(x)'], alias: { From acb1c9de60ee17730a15cd2be01779bada283d9d Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Fri, 10 Nov 2023 22:55:22 -0600 Subject: [PATCH 05/50] Add `testUtils` file --- test/testUtils.ts | 404 +++++++++++++++++++++++----------------------- 1 file changed, 202 insertions(+), 202 deletions(-) diff --git a/test/testUtils.ts b/test/testUtils.ts index eb48e6a08..f89a7507a 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -1,5 +1,5 @@ -import type { PayloadAction } from '@reduxjs/toolkit' -import { combineReducers, configureStore, createSlice } from '@reduxjs/toolkit' +import type { PayloadAction } from '@reduxjs/toolkit' +import { combineReducers, configureStore, createSlice } from '@reduxjs/toolkit' import { test } from 'vitest' import type { AnyFunction, @@ -8,21 +8,21 @@ import type { SelectorArray, Simplify } from '../src/types' - -interface Todo { - id: number - title: string - description: string - completed: boolean -} - -interface Alert { - id: number - message: string - type: string - read: boolean -} - + +interface Todo { + id: number + title: string + description: string + completed: boolean +} + +interface Alert { + id: number + message: string + type: string + read: boolean +} + interface BillingAddress { street: string city: string @@ -80,50 +80,50 @@ interface UserState { } // For long arrays -const todoState = [ - { - id: 0, - title: 'Buy groceries', - description: 'Milk, bread, eggs, and fruits', - completed: false - }, - { - id: 1, - title: 'Schedule dentist appointment', - description: 'Check available slots for next week', - completed: false - }, - { - id: 2, - title: 'Convince the cat to get a job', - description: 'Need extra income for cat treats', - completed: false - }, - { - id: 3, - title: 'Figure out if plants are plotting world domination', - description: 'That cactus looks suspicious...', - completed: false - }, - { - id: 4, - title: 'Practice telekinesis', - description: 'Try moving the remote without getting up', - completed: false - }, - { - id: 5, - title: 'Determine location of El Dorado', - description: 'Might need it for the next vacation', - completed: false - }, - { - id: 6, - title: 'Master the art of invisible potato juggling', - description: 'Great party trick', - completed: false - } -] +const todoState = [ + { + id: 0, + title: 'Buy groceries', + description: 'Milk, bread, eggs, and fruits', + completed: false + }, + { + id: 1, + title: 'Schedule dentist appointment', + description: 'Check available slots for next week', + completed: false + }, + { + id: 2, + title: 'Convince the cat to get a job', + description: 'Need extra income for cat treats', + completed: false + }, + { + id: 3, + title: 'Figure out if plants are plotting world domination', + description: 'That cactus looks suspicious...', + completed: false + }, + { + id: 4, + title: 'Practice telekinesis', + description: 'Try moving the remote without getting up', + completed: false + }, + { + id: 5, + title: 'Determine location of El Dorado', + description: 'Might need it for the next vacation', + completed: false + }, + { + id: 6, + title: 'Master the art of invisible potato juggling', + description: 'Great party trick', + completed: false + } +] export const createTodoItem = (id: number) => { return { @@ -147,49 +147,49 @@ pushToTodos(200) // for (let i = todoStateLength; i < 200; i++) { // todoState.push(createTodoItem(i)) // } - -const alertState = [ - { - id: 0, - message: 'You have an upcoming meeting at 3 PM.', - type: 'reminder', - read: false - }, - { - id: 1, - message: 'New software update available.', - type: 'notification', - read: false - }, - { - id: 3, - message: - 'The plants have been watered, but keep an eye on that shifty cactus.', - type: 'notification', - read: false - }, - { - id: 4, - message: - 'Telekinesis class has been moved to 5 PM. Please do not bring any spoons.', - type: 'reminder', - read: false - }, - { - id: 5, - message: - 'Expedition to El Dorado is postponed. The treasure map is being updated.', - type: 'notification', - read: false - }, - { - id: 6, - message: - 'Invisible potato juggling championship is tonight. May the best mime win.', - type: 'reminder', - read: false - } -] + +const alertState = [ + { + id: 0, + message: 'You have an upcoming meeting at 3 PM.', + type: 'reminder', + read: false + }, + { + id: 1, + message: 'New software update available.', + type: 'notification', + read: false + }, + { + id: 3, + message: + 'The plants have been watered, but keep an eye on that shifty cactus.', + type: 'notification', + read: false + }, + { + id: 4, + message: + 'Telekinesis class has been moved to 5 PM. Please do not bring any spoons.', + type: 'reminder', + read: false + }, + { + id: 5, + message: + 'Expedition to El Dorado is postponed. The treasure map is being updated.', + type: 'notification', + read: false + }, + { + id: 6, + message: + 'Invisible potato juggling championship is tonight. May the best mime win.', + type: 'reminder', + read: false + } +] // For nested fields tests const userState: UserState = { @@ -233,54 +233,54 @@ const userState: UserState = { language: 'en-US' } } - -const todoSlice = createSlice({ - name: 'todos', - initialState: todoState, - reducers: { - toggleCompleted: (state, action: PayloadAction) => { - const todo = state.find(todo => todo.id === action.payload) - if (todo) { - todo.completed = !todo.completed - } - }, - - addTodo: (state, action: PayloadAction>) => { - const newId = state.length > 0 ? state[state.length - 1].id + 1 : 0 - state.push({ - ...action.payload, - id: newId, - completed: false - }) - }, - - removeTodo: (state, action: PayloadAction) => { - return state.filter(todo => todo.id !== action.payload) - }, - - updateTodo: (state, action: PayloadAction) => { - const index = state.findIndex(todo => todo.id === action.payload.id) - if (index !== -1) { - state[index] = action.payload - } - }, - - clearCompleted: state => { - return state.filter(todo => !todo.completed) - } - } -}) - -const alertSlice = createSlice({ - name: 'alerts', - initialState: alertState, - reducers: { - markAsRead: (state, action: PayloadAction) => { - const alert = state.find(alert => alert.id === action.payload) - if (alert) { - alert.read = true - } - }, + +const todoSlice = createSlice({ + name: 'todos', + initialState: todoState, + reducers: { + toggleCompleted: (state, action: PayloadAction) => { + const todo = state.find(todo => todo.id === action.payload) + if (todo) { + todo.completed = !todo.completed + } + }, + + addTodo: (state, action: PayloadAction>) => { + const newId = state.length > 0 ? state[state.length - 1].id + 1 : 0 + state.push({ + ...action.payload, + id: newId, + completed: false + }) + }, + + removeTodo: (state, action: PayloadAction) => { + return state.filter(todo => todo.id !== action.payload) + }, + + updateTodo: (state, action: PayloadAction) => { + const index = state.findIndex(todo => todo.id === action.payload.id) + if (index !== -1) { + state[index] = action.payload + } + }, + + clearCompleted: state => { + return state.filter(todo => !todo.completed) + } + } +}) + +const alertSlice = createSlice({ + name: 'alerts', + initialState: alertState, + reducers: { + markAsRead: (state, action: PayloadAction) => { + const alert = state.find(alert => alert.id === action.payload) + if (alert) { + alert.read = true + } + }, toggleRead: (state, action: PayloadAction) => { const alert = state.find(alert => alert.id === action.payload) @@ -288,20 +288,20 @@ const alertSlice = createSlice({ alert.read = !alert.read } }, - - addAlert: (state, action: PayloadAction>) => { - const newId = state.length > 0 ? state[state.length - 1].id + 1 : 0 - state.push({ - ...action.payload, - id: newId - }) - }, - - removeAlert: (state, action: PayloadAction) => { - return state.filter(alert => alert.id !== action.payload) - } - } -}) + + addAlert: (state, action: PayloadAction>) => { + const newId = state.length > 0 ? state[state.length - 1].id + 1 : 0 + state.push({ + ...action.payload, + id: newId + }) + }, + + removeAlert: (state, action: PayloadAction) => { + return state.filter(alert => alert.id !== action.payload) + } + } +}) const userSlice = createSlice({ name: 'users', @@ -364,52 +364,52 @@ const userSlice = createSlice({ } } }) - -const rootReducer = combineReducers({ - [todoSlice.name]: todoSlice.reducer, + +const rootReducer = combineReducers({ + [todoSlice.name]: todoSlice.reducer, [alertSlice.name]: alertSlice.reducer, [userSlice.name]: userSlice.reducer -}) - +}) + export const setupStore = (preloadedState?: Partial) => { return configureStore({ reducer: rootReducer, preloadedState }) } - -export type AppStore = Simplify> - + +export type AppStore = Simplify> + export type RootState = ReturnType - -export interface LocalTestContext { - store: AppStore - state: RootState -} - + +export interface LocalTestContext { + store: AppStore + state: RootState +} + export const { markAsRead, addAlert, removeAlert, toggleRead } = alertSlice.actions - -export const { - toggleCompleted, - addTodo, - removeTodo, - updateTodo, - clearCompleted -} = todoSlice.actions + +export const { + toggleCompleted, + addTodo, + removeTodo, + updateTodo, + clearCompleted +} = todoSlice.actions export const { setUserName, setUserEmail, setAppTheme } = userSlice.actions - -// Since Node 16 does not support `structuredClone` -export const deepClone = (object: T): T => - JSON.parse(JSON.stringify(object)) - -export const setFunctionName = (func: AnyFunction, name: string) => { - Object.defineProperty(func, 'name', { value: name }) -} - -export const setFunctionNames = (funcObject: Record) => { - Object.entries(funcObject).forEach(([key, value]) => - setFunctionName(value, key) - ) -} + +// Since Node 16 does not support `structuredClone` +export const deepClone = (object: T): T => + JSON.parse(JSON.stringify(object)) + +export const setFunctionName = (func: AnyFunction, name: string) => { + Object.defineProperty(func, 'name', { value: name }) +} + +export const setFunctionNames = (funcObject: Record) => { + Object.entries(funcObject).forEach(([key, value]) => + setFunctionName(value, key) + ) +} const store = setupStore() const state = store.getState() From 94fef7613aafd06fabac62feead573404039434b Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Fri, 10 Nov 2023 23:02:07 -0600 Subject: [PATCH 06/50] Add `createCurriedSelector` --- src/createCurriedSelectorCreator.ts | 246 ++++++++++++++-------------- 1 file changed, 123 insertions(+), 123 deletions(-) diff --git a/src/createCurriedSelectorCreator.ts b/src/createCurriedSelectorCreator.ts index f842e6ee4..7f09f22ca 100644 --- a/src/createCurriedSelectorCreator.ts +++ b/src/createCurriedSelectorCreator.ts @@ -1,132 +1,132 @@ -import type { CreateSelectorFunction } from './createSelectorCreator' -import { createSelectorCreator } from './createSelectorCreator' - -import { defaultMemoize } from './defaultMemoize' -import type { - Combiner, - CreateSelectorOptions, - CurriedOutputSelector, - DropFirstParameter, - InterruptRecursion, - SelectorArray, - Simplify, - UnknownMemoizer -} from './types' - +import type { CreateSelectorFunction } from './createSelectorCreator' +import { createSelectorCreator } from './createSelectorCreator' + +import { defaultMemoize } from './defaultMemoize' +import type { + Combiner, + CreateSelectorOptions, + CurriedOutputSelector, + DropFirstParameter, + InterruptRecursion, + SelectorArray, + Simplify, + UnknownMemoizer +} from './types' + /** * @WIP */ -export interface CreateCurriedSelector< - MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, - ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize -> { - /** - * One arg - */ - ( - ...createSelectorArgs: [ - ...inputSelectors: InputSelectors, - combiner: Combiner - ] - ): CurriedOutputSelector< - InputSelectors, - Result, - MemoizeFunction, - ArgsMemoizeFunction - > & - InterruptRecursion - - /** - * inline args - */ - < - InputSelectors extends SelectorArray, - Result, - OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction, - OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction - >( - ...createSelectorArgs: [ - ...inputSelectors: InputSelectors, - combiner: Combiner, - createSelectorOptions: Simplify< - CreateSelectorOptions< - MemoizeFunction, - ArgsMemoizeFunction, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction - > - > - ] - ): CurriedOutputSelector< - InputSelectors, - Result, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction - > & - InterruptRecursion - - /** - * array args - */ - < - InputSelectors extends SelectorArray, - Result, - OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction, - OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction - >( - inputSelectors: [...InputSelectors], - combiner: Combiner, - createSelectorOptions?: Simplify< - CreateSelectorOptions< - MemoizeFunction, - ArgsMemoizeFunction, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction - > - > - ): CurriedOutputSelector< - InputSelectors, - Result, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction - > & - InterruptRecursion -} - +export interface CreateCurriedSelector< + MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize +> { + /** + * One arg + */ + ( + ...createSelectorArgs: [ + ...inputSelectors: InputSelectors, + combiner: Combiner + ] + ): CurriedOutputSelector< + InputSelectors, + Result, + MemoizeFunction, + ArgsMemoizeFunction + > & + InterruptRecursion + + /** + * inline args + */ + < + InputSelectors extends SelectorArray, + Result, + OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction, + OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction + >( + ...createSelectorArgs: [ + ...inputSelectors: InputSelectors, + combiner: Combiner, + createSelectorOptions: Simplify< + CreateSelectorOptions< + MemoizeFunction, + ArgsMemoizeFunction, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > + > + ] + ): CurriedOutputSelector< + InputSelectors, + Result, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > & + InterruptRecursion + + /** + * array args + */ + < + InputSelectors extends SelectorArray, + Result, + OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction, + OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction + >( + inputSelectors: [...InputSelectors], + combiner: Combiner, + createSelectorOptions?: Simplify< + CreateSelectorOptions< + MemoizeFunction, + ArgsMemoizeFunction, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > + > + ): CurriedOutputSelector< + InputSelectors, + Result, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > & + InterruptRecursion +} + /** * @WIP */ -export function createCurriedSelectorCreator< - MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, - ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize ->(...createSelectorCreatorArgs: Parameters) { - const createSelector = createSelectorCreator(...createSelectorCreatorArgs) - - const createCurriedSelector = ( - ...createSelectorArgs: Parameters< - CreateSelectorFunction - > - ) => { - // @ts-ignore - const selector = createSelector.apply(null, createSelectorArgs) - // const selector = createSelector(...createSelectorArgs) - const curriedSelector = selector.argsMemoize( - (...params: DropFirstParameter) => { - return selector.argsMemoize((state: Parameters[0]) => { - return selector(state, ...params) - }) - } - ) - return Object.assign(curriedSelector, selector) as CurriedOutputSelector - } - return createCurriedSelector as unknown as CreateCurriedSelector< - MemoizeFunction, - ArgsMemoizeFunction - > -} - +export function createCurriedSelectorCreator< + MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize +>(...createSelectorCreatorArgs: Parameters) { + const createSelector = createSelectorCreator(...createSelectorCreatorArgs) + + const createCurriedSelector = ( + ...createSelectorArgs: Parameters< + CreateSelectorFunction + > + ) => { + // @ts-ignore + const selector = createSelector.apply(null, createSelectorArgs) + // const selector = createSelector(...createSelectorArgs) + const curriedSelector = selector.argsMemoize( + (...params: DropFirstParameter) => { + return selector.argsMemoize((state: Parameters[0]) => { + return selector(state, ...params) + }) + } + ) + return Object.assign(curriedSelector, selector) as CurriedOutputSelector + } + return createCurriedSelector as unknown as CreateCurriedSelector< + MemoizeFunction, + ArgsMemoizeFunction + > +} + /** * @WIP */ -export const createCurriedSelector = - /* #__PURE__ */ createCurriedSelectorCreator(defaultMemoize) +export const createCurriedSelector = + /* #__PURE__ */ createCurriedSelectorCreator(defaultMemoize) From 93820c6a37f61f0b4db479c440183719b92085e2 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Fri, 10 Nov 2023 23:26:35 -0600 Subject: [PATCH 07/50] Fix issues with topic expansion in docs. --- README.md | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 919167177..4f857ba1e 100644 --- a/README.md +++ b/README.md @@ -1477,6 +1477,8 @@ A: Check that your memoization function is compatible with your state update fun ### Why is my selector recomputing when the input state stays the same? + + A: Make sure you have `inputStabilityCheck` set to either `always` or `once` and that in and of itself should take some weight off of your shoulders by doing some of the debugging for you. Also make sure you use your `output selector fields` like `recomputations`, `resetRecomputations`, `dependencyRecomputations`, `resetDependencyRecomputations` to narrow down the root of the problem to see where it is coming from. Is it coming from the arguments changing reference unexpectedly? then if that is the case your `dependencyRecomputations` should be going up. If `dependencyRecomputations` is incrementing but `recomputations` is not, that means your arguments are changing reference too often. If your `input selectors` return a new reference every time, that will be caught be `inputStabilityCheck`. And if your arguments are changing reference too often, you can narrow it down to see which arguments are changing reference too often by doing this: ```ts @@ -1505,8 +1507,6 @@ const selectAlertsByType = createSelector( ) ``` - -
@@ -1527,21 +1527,14 @@ A: Yes. `Reselect` has no dependencies on any other package, so although it was ### How do I create a selector that takes an argument? + + When creating a selector that accepts arguments in `Reselect`, it's important to structure your input and `output selectors` appropriately. Here are key points to consider: 1. **Consistency in Arguments**: Ensure that all positional arguments across `input selectors` are of the same type for consistency. 2. **Selective Argument Usage**: Design each selector to use only its relevant argument(s) and ignore the rest. This is crucial because all `input selectors` receive the same arguments that are passed to the `output selector`. - - Suppose we have the following state structure: ```ts @@ -1584,8 +1577,6 @@ const finalResult = items.filter(item => item.category === category && item.id !== id) ``` - -
@@ -1606,6 +1597,8 @@ A: We think it works great for a lot of use cases, but sure. See [these examples ### How do I test a selector? + + A: For a given input, a selector should always produce the same result. For this reason they are simple to unit test. ```ts @@ -1647,8 +1640,6 @@ test('selector unit test', () => { }) ``` - -
From 212e889aeda92f12f5b0bc54d0fb4b68ad888cb2 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sat, 11 Nov 2023 00:07:57 -0600 Subject: [PATCH 08/50] Resolve merge conflicts --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1f30c9c3a..950731b26 100644 --- a/README.md +++ b/README.md @@ -1531,8 +1531,8 @@ Conceptually, Reselect works like this internally: ```ts const finalSelector = (...args) => { - const extractedValues = inputFunctions.map(input => input(...args)); - return output(...extractedValues); + const extractedValues = inputFunctions.map(input => input(...args)) + return output(...extractedValues) } ``` @@ -1588,8 +1588,7 @@ const finalResult = items.filter(item => item.category === category && item.id !== id) ``` - -More generally, you can have N arguments passed to the selector, and you can have M input functions extracting values from any of those arguments. All M extracted values get passed to the output function. +More generally, you can have N arguments passed to the selector, and you can have M input functions extracting values from any of those arguments. All M extracted values get passed to the output function. ### The default memoization function is no good, can I use a different one? From c6dab42c176af270e953905ada0729c5a798d72d Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sat, 11 Nov 2023 00:12:18 -0600 Subject: [PATCH 09/50] Fix docs for selectors that take arguments --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 950731b26..04283c3c3 100644 --- a/README.md +++ b/README.md @@ -1527,6 +1527,8 @@ A: Yes. `Reselect` has no dependencies on any other package, so although it was ### Q: How do I create a selector that takes an argument? + + Conceptually, Reselect works like this internally: ```ts @@ -1538,8 +1540,6 @@ const finalSelector = (...args) => { In other words, all the arguments passed to the selector function are immediately passed to all of the inputs. - - When creating a selector that accepts arguments in `Reselect`, it's important to structure your input and `output selectors` appropriately. Here are key points to consider: 1. **Consistency in Arguments**: Ensure that all positional arguments across `input selectors` are of the same type for consistency. From 5b30a5a1888995182f95fcd95c683ce859163fa3 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Mon, 13 Nov 2023 06:50:41 -0600 Subject: [PATCH 10/50] Add `@typescript/analyze-trace` to `devDependencies` - Add `@typescript/analyze-trace` to measure TS performance bottlenecks. --- .gitignore | 4 +- package.json | 3 +- yarn.lock | 942 +++++++++++++++++++++++++++------------------------ 3 files changed, 505 insertions(+), 444 deletions(-) diff --git a/.gitignore b/.gitignore index 9876dbe03..d288a6f6e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ typescript_test/common.js flow_test/should_fail/flow-typed/index.js.flow flow_test/should_pass/flow-typed/index.js.flow reselect-builds/ - +trace typesversions .cache @@ -24,4 +24,4 @@ typesversions !.yarn/sdks !.yarn/versions .pnp.* -*.tgz \ No newline at end of file +*.tgz diff --git a/package.json b/package.json index a2e4a8d16..62f7f7ddd 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "test": "node --expose-gc ./node_modules/vitest/dist/cli-wrapper.js run", "test:cov": "vitest run --coverage", "type-check": "vitest --run typecheck", - "test:typescript": "tsc --noEmit -p typescript_test/tsconfig.json" + "test:typescript": "tsc --noEmit -p typescript_test/tsconfig.json --generateTrace trace && npx @typescript/analyze-trace trace && rimraf trace" }, "keywords": [ "react", @@ -55,6 +55,7 @@ "@typescript-eslint/eslint-plugin": "5.1.0", "@typescript-eslint/eslint-plugin-tslint": "5.1.0", "@typescript-eslint/parser": "5.1.0", + "@typescript/analyze-trace": "^0.10.1", "eslint": "^8.0.1", "eslint-plugin-react": "^7.26.1", "eslint-plugin-typescript": "0.14.0", diff --git a/yarn.lock b/yarn.lock index 8004e2ca8..3a1dc4d72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13,11 +13,11 @@ __metadata: linkType: hard "@babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.9.2": - version: 7.23.1 - resolution: "@babel/runtime@npm:7.23.1" + version: 7.23.2 + resolution: "@babel/runtime@npm:7.23.2" dependencies: regenerator-runtime: ^0.14.0 - checksum: 0cd0d43e6e7dc7f9152fda8c8312b08321cda2f56ef53d6c22ebdd773abdc6f5d0a69008de90aa41908d00e2c1facb24715ff121274e689305c858355ff02c70 + checksum: 6c4df4839ec75ca10175f636d6362f91df8a3137f86b38f6cd3a4c90668a0fe8e9281d320958f4fbd43b394988958585a17c3aab2a4ea6bf7316b22916a371fb languageName: node linkType: hard @@ -341,15 +341,15 @@ __metadata: linkType: hard "@eslint-community/regexpp@npm:^4.6.1": - version: 4.9.0 - resolution: "@eslint-community/regexpp@npm:4.9.0" - checksum: 82411f0643ab9bfd271bf12c8c75031266b13595d9371585ee3b0d680d918d4abf37c7e94d0da22e45817c9bbc59b79dfcbd672050dfb00af88fb89c80fd420f + version: 4.10.0 + resolution: "@eslint-community/regexpp@npm:4.10.0" + checksum: 2a6e345429ea8382aaaf3a61f865cae16ed44d31ca917910033c02dc00d505d939f10b81e079fa14d43b51499c640138e153b7e40743c4c094d9df97d4e56f7b languageName: node linkType: hard -"@eslint/eslintrc@npm:^2.1.2": - version: 2.1.2 - resolution: "@eslint/eslintrc@npm:2.1.2" +"@eslint/eslintrc@npm:^2.1.3": + version: 2.1.3 + resolution: "@eslint/eslintrc@npm:2.1.3" dependencies: ajv: ^6.12.4 debug: ^4.3.2 @@ -360,25 +360,25 @@ __metadata: js-yaml: ^4.1.0 minimatch: ^3.1.2 strip-json-comments: ^3.1.1 - checksum: bc742a1e3b361f06fedb4afb6bf32cbd27171292ef7924f61c62f2aed73048367bcc7ac68f98c06d4245cd3fabc43270f844e3c1699936d4734b3ac5398814a7 + checksum: 5c6c3878192fe0ddffa9aff08b4e2f3bcc8f1c10d6449b7295a5f58b662019896deabfc19890455ffd7e60a5bd28d25d0eaefb2f78b2d230aae3879af92b89e5 languageName: node linkType: hard -"@eslint/js@npm:8.50.0": - version: 8.50.0 - resolution: "@eslint/js@npm:8.50.0" - checksum: 302478f2acaaa7228729ec6a04f56641590185e1d8cd1c836a6db8a6b8009f80a57349341be9fbb9aa1721a7a569d1be3ffc598a33300d22816f11832095386c +"@eslint/js@npm:8.53.0": + version: 8.53.0 + resolution: "@eslint/js@npm:8.53.0" + checksum: e0d5cfb0000aaee237c8e6d6d6e366faa60b1ef7f928ce17778373aa44d3b886368f6d5e1f97f913f0f16801aad016db8b8df78418c9d18825c15590328028af languageName: node linkType: hard -"@humanwhocodes/config-array@npm:^0.11.11": - version: 0.11.11 - resolution: "@humanwhocodes/config-array@npm:0.11.11" +"@humanwhocodes/config-array@npm:^0.11.13": + version: 0.11.13 + resolution: "@humanwhocodes/config-array@npm:0.11.13" dependencies: - "@humanwhocodes/object-schema": ^1.2.1 + "@humanwhocodes/object-schema": ^2.0.1 debug: ^4.1.1 minimatch: ^3.0.5 - checksum: db84507375ab77b8ffdd24f498a5b49ad6b64391d30dd2ac56885501d03964d29637e05b1ed5aefa09d57ac667e28028bc22d2da872bfcd619652fbdb5f4ca19 + checksum: f8ea57b0d7ed7f2d64cd3944654976829d9da91c04d9c860e18804729a33f7681f78166ef4c761850b8c324d362f7d53f14c5c44907a6b38b32c703ff85e4805 languageName: node linkType: hard @@ -389,10 +389,10 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/object-schema@npm:^1.2.1": - version: 1.2.1 - resolution: "@humanwhocodes/object-schema@npm:1.2.1" - checksum: a824a1ec31591231e4bad5787641f59e9633827d0a2eaae131a288d33c9ef0290bd16fda8da6f7c0fcb014147865d12118df10db57f27f41e20da92369fcb3f1 +"@humanwhocodes/object-schema@npm:^2.0.1": + version: 2.0.1 + resolution: "@humanwhocodes/object-schema@npm:2.0.1" + checksum: 24929487b1ed48795d2f08346a0116cc5ee4634848bce64161fb947109352c562310fd159fc64dda0e8b853307f5794605191a9547f7341158559ca3c8262a45 languageName: node linkType: hard @@ -452,12 +452,12 @@ __metadata: linkType: hard "@jridgewell/trace-mapping@npm:^0.3.9": - version: 0.3.19 - resolution: "@jridgewell/trace-mapping@npm:0.3.19" + version: 0.3.20 + resolution: "@jridgewell/trace-mapping@npm:0.3.20" dependencies: "@jridgewell/resolve-uri": ^3.1.0 "@jridgewell/sourcemap-codec": ^1.4.14 - checksum: 956a6f0f6fec060fb48c6bf1f5ec2064e13cd38c8be3873877d4b92b4a27ba58289a34071752671262a3e3c202abcc3fa2aac64d8447b4b0fa1ba3c9047f1c20 + checksum: cd1a7353135f385909468ff0cf20bdd37e59f2ee49a13a966dedf921943e222082c583ade2b579ff6cd0d8faafcb5461f253e1bf2a9f48fec439211fdbe788f5 languageName: node linkType: hard @@ -488,6 +488,19 @@ __metadata: languageName: node linkType: hard +"@npmcli/agent@npm:^2.0.0": + version: 2.2.0 + resolution: "@npmcli/agent@npm:2.2.0" + dependencies: + agent-base: ^7.1.0 + http-proxy-agent: ^7.0.0 + https-proxy-agent: ^7.0.1 + lru-cache: ^10.0.1 + socks-proxy-agent: ^8.0.1 + checksum: 3b25312edbdfaa4089af28e2d423b6f19838b945e47765b0c8174c1395c79d43c3ad6d23cb364b43f59fd3acb02c93e3b493f72ddbe3dfea04c86843a7311fc4 + languageName: node + linkType: hard + "@npmcli/fs@npm:^3.1.0": version: 3.1.0 resolution: "@npmcli/fs@npm:3.1.0" @@ -505,8 +518,8 @@ __metadata: linkType: hard "@reduxjs/toolkit@npm:^1.9.3": - version: 1.9.6 - resolution: "@reduxjs/toolkit@npm:1.9.6" + version: 1.9.7 + resolution: "@reduxjs/toolkit@npm:1.9.7" dependencies: immer: ^9.0.21 redux: ^4.2.1 @@ -520,90 +533,90 @@ __metadata: optional: true react-redux: optional: true - checksum: 61d445f7e084c79f9601f61fcfc4eb65152b850b2a4330239d982297605bd870e63dc1e0211deb3822392cd3bc0c88ca0cdb236a9711a4311dfb199c607b6ac5 + checksum: ac25dec73a5d2df9fc7fbe98c14ccc73919e5ee1d6f251db0d2ec8f90273f92ef39c26716704bf56b5a40189f72d94b4526dc3a8c7ac3986f5daf44442bcc364 languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.1.5": - version: 4.1.5 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.1.5" +"@rollup/rollup-android-arm-eabi@npm:4.4.0": + version: 4.4.0 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.4.0" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.1.5": - version: 4.1.5 - resolution: "@rollup/rollup-android-arm64@npm:4.1.5" +"@rollup/rollup-android-arm64@npm:4.4.0": + version: 4.4.0 + resolution: "@rollup/rollup-android-arm64@npm:4.4.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.1.5": - version: 4.1.5 - resolution: "@rollup/rollup-darwin-arm64@npm:4.1.5" +"@rollup/rollup-darwin-arm64@npm:4.4.0": + version: 4.4.0 + resolution: "@rollup/rollup-darwin-arm64@npm:4.4.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.1.5": - version: 4.1.5 - resolution: "@rollup/rollup-darwin-x64@npm:4.1.5" +"@rollup/rollup-darwin-x64@npm:4.4.0": + version: 4.4.0 + resolution: "@rollup/rollup-darwin-x64@npm:4.4.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.1.5": - version: 4.1.5 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.1.5" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.4.0": + version: 4.4.0 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.4.0" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.1.5": - version: 4.1.5 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.1.5" +"@rollup/rollup-linux-arm64-gnu@npm:4.4.0": + version: 4.4.0 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.4.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.1.5": - version: 4.1.5 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.1.5" +"@rollup/rollup-linux-arm64-musl@npm:4.4.0": + version: 4.4.0 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.4.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.1.5": - version: 4.1.5 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.1.5" +"@rollup/rollup-linux-x64-gnu@npm:4.4.0": + version: 4.4.0 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.4.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.1.5": - version: 4.1.5 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.1.5" +"@rollup/rollup-linux-x64-musl@npm:4.4.0": + version: 4.4.0 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.4.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.1.5": - version: 4.1.5 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.1.5" +"@rollup/rollup-win32-arm64-msvc@npm:4.4.0": + version: 4.4.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.4.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.1.5": - version: 4.1.5 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.1.5" +"@rollup/rollup-win32-ia32-msvc@npm:4.4.0": + version: 4.4.0 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.4.0" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.1.5": - version: 4.1.5 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.1.5" +"@rollup/rollup-win32-x64-msvc@npm:4.4.0": + version: 4.4.0 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.4.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -615,33 +628,19 @@ __metadata: languageName: node linkType: hard -"@tootallnate/once@npm:2": - version: 2.0.0 - resolution: "@tootallnate/once@npm:2.0.0" - checksum: ad87447820dd3f24825d2d947ebc03072b20a42bfc96cbafec16bff8bbda6c1a81fcb0be56d5b21968560c5359a0af4038a68ba150c3e1694fe4c109a063bed8 - languageName: node - linkType: hard - "@types/chai-subset@npm:^1.3.3": - version: 1.3.3 - resolution: "@types/chai-subset@npm:1.3.3" + version: 1.3.5 + resolution: "@types/chai-subset@npm:1.3.5" dependencies: "@types/chai": "*" - checksum: 4481da7345022995f5a105e6683744f7203d2c3d19cfe88d8e17274d045722948abf55e0adfd97709e0f043dade37a4d4e98cd4c660e2e8a14f23e6ecf79418f + checksum: 715c46d3e90f87482c2769389d560456bb257b225716ff44c275c231bdb62c8a30629f355f412bac0ecab07ebc036c1806d9ed9dde9792254f8ef4f07f76033b languageName: node linkType: hard -"@types/chai@npm:*": - version: 4.3.6 - resolution: "@types/chai@npm:4.3.6" - checksum: 32a6c18bf53fb3dbd89d1bfcadb1c6fd45cc0007c34e436393cc37a0a5a556f9e6a21d1e8dd71674c40cc36589d2f30bf4d9369d7787021e54d6e997b0d7300a - languageName: node - linkType: hard - -"@types/chai@npm:^4.3.5": - version: 4.3.9 - resolution: "@types/chai@npm:4.3.9" - checksum: 2300a2c7abd4cb590349927a759b3d0172211a69f363db06e585faf7874a47f125ef3b364cce4f6190e3668147587fc11164c791c9560cf9bce8478fb7019610 +"@types/chai@npm:*, @types/chai@npm:^4.3.5": + version: 4.3.10 + resolution: "@types/chai@npm:4.3.10" + checksum: cb9ebe31f5da2d72c4b9362ec4efb33497355372270163c0290f6b9c389934ff178dac933be6b2911a125f15972c0379603736ea83ad10bfca933b6aaf6c0c5b languageName: node linkType: hard @@ -656,26 +655,26 @@ __metadata: linkType: hard "@types/hoist-non-react-statics@npm:^3.3.1": - version: 3.3.2 - resolution: "@types/hoist-non-react-statics@npm:3.3.2" + version: 3.3.5 + resolution: "@types/hoist-non-react-statics@npm:3.3.5" dependencies: "@types/react": "*" hoist-non-react-statics: ^3.3.0 - checksum: fe5d4b751e13f56010811fd6c4e49e53e2ccbcbbdc54bb8d86a413fbd08c5a83311bca9ef75a1a88d3ba62806711b5dea3f323c0e0f932b3a283dcebc3240238 + checksum: b645b062a20cce6ab1245ada8274051d8e2e0b2ee5c6bd58215281d0ec6dae2f26631af4e2e7c8abe238cdcee73fcaededc429eef569e70908f82d0cc0ea31d7 languageName: node linkType: hard "@types/json-schema@npm:^7.0.9": - version: 7.0.13 - resolution: "@types/json-schema@npm:7.0.13" - checksum: 345df21a678fa72fb389f35f33de77833d09d4a142bb2bcb27c18690efa4cf70fc2876e43843cefb3fbdb9fcb12cd3e970a90936df30f53bbee899865ff605ab + version: 7.0.15 + resolution: "@types/json-schema@npm:7.0.15" + checksum: 97ed0cb44d4070aecea772b7b2e2ed971e10c81ec87dd4ecc160322ffa55ff330dace1793489540e3e318d90942064bb697cc0f8989391797792d919737b3b98 languageName: node linkType: hard "@types/lodash@npm:^4.14.175": - version: 4.14.199 - resolution: "@types/lodash@npm:4.14.199" - checksum: e68d1fcbbfce953ed87b296a628573f62939227bcda0c934954e862b421e8a34c5e71cad6fea27b9980567909e6a4698f09025692958e36d64ea9ed99ec6fb2e + version: 4.14.201 + resolution: "@types/lodash@npm:4.14.201" + checksum: 484be655298e9b2dc2d218ea934071b2ea31e4a531c561dd220dbda65237e8d08c20dc2d457ac24f29be7fe167415bf7bb9360ea0d80bdb8b0f0ec8d8db92fae languageName: node linkType: hard @@ -687,44 +686,46 @@ __metadata: linkType: hard "@types/node@npm:*": - version: 20.8.0 - resolution: "@types/node@npm:20.8.0" - checksum: ebad6342d54238a24bf980d7750117a5d67749c9b72cbb7a974a1e932c39034aa3a810d669e007e8a5071782a253aa069a187b614407a382403c9826e837c849 + version: 20.9.0 + resolution: "@types/node@npm:20.9.0" + dependencies: + undici-types: ~5.26.4 + checksum: bfd927da6bff8a32051fa44bb47ca32a56d2c8bc8ba0170770f181cc1fa3c0b05863c9b930f0ba8604a48d5eb0d319166601709ca53bf2deae0025d8b6c6b8a3 languageName: node linkType: hard "@types/prop-types@npm:*": - version: 15.7.8 - resolution: "@types/prop-types@npm:15.7.8" - checksum: 61dfad79da8b1081c450bab83b77935df487ae1cdd4660ec7df6be8e74725c15fa45cf486ce057addc956ca4ae78300b97091e2a25061133d1b9a1440bc896ae + version: 15.7.10 + resolution: "@types/prop-types@npm:15.7.10" + checksum: 39ecc2d9e439ed16b32937a08d98b84ed4a70f53bcd52c8564c0cd7a36fe1004ca83a1fb94b13c1b7a5c048760f06445c3c6a91a6972c8eff652c0b50c9424b1 languageName: node linkType: hard "@types/react@npm:*": - version: 18.2.24 - resolution: "@types/react@npm:18.2.24" + version: 18.2.37 + resolution: "@types/react@npm:18.2.37" dependencies: "@types/prop-types": "*" "@types/scheduler": "*" csstype: ^3.0.2 - checksum: ea5d8204e71b1c9c6631f429a93f8e7be0614cdbdb464e92b3181bdccd8a7c45e30ded8b13da726684b6393f651317c36d54832e3d3cdea0da480a3f26268909 + checksum: 2d2599f1a09e4f678509161fea8baeaf76d21deee460f4f3ccc1ca431ebe85f896d7d0b906127de17e97ed57240cec61955eb97d0b5d9cbf4e97fd6620b1acdb languageName: node linkType: hard "@types/scheduler@npm:*": - version: 0.16.4 - resolution: "@types/scheduler@npm:0.16.4" - checksum: a57b0f10da1b021e6bd5eeef8a1917dd3b08a8715bd8029e2ded2096d8f091bb1bb1fef2d66e139588a983c4bfbad29b59e48011141725fa83c76e986e1257d7 + version: 0.16.6 + resolution: "@types/scheduler@npm:0.16.6" + checksum: 4cec89727584a50c66a07c322469a4d9e64f5b0117691f36afd4ceae75741c0038a6e107c05e515511d5358b5897becbe065b6e4560664cb1b16f6754915043d languageName: node linkType: hard "@types/shelljs@npm:^0.8.11": - version: 0.8.13 - resolution: "@types/shelljs@npm:0.8.13" + version: 0.8.15 + resolution: "@types/shelljs@npm:0.8.15" dependencies: "@types/glob": ~7.2.0 "@types/node": "*" - checksum: 86ac87f4688aeb578c3a85fad1c6b4a740f3951d4c9b1d5e5fd9240225589b2c7a10aacff5c857a5f12961e55ffef40698a46e41d7b27cbdf9cf4bcaf1349a7a + checksum: 94939421c6c83d3075e1c56bf940eb3c34567c6b2ac0b553ec81de7f4c7e7cdfc729117d821c22418d64c45fcd4f96a6ec7ae21ed0d7a80e3e9a008672dde35f languageName: node linkType: hard @@ -849,6 +850,33 @@ __metadata: languageName: node linkType: hard +"@typescript/analyze-trace@npm:^0.10.1": + version: 0.10.1 + resolution: "@typescript/analyze-trace@npm:0.10.1" + dependencies: + chalk: ^4.1.2 + exit: ^0.1.2 + jsonparse: ^1.3.1 + jsonstream-next: ^3.0.0 + p-limit: ^3.1.0 + split2: ^3.2.2 + treeify: ^1.1.0 + yargs: ^16.2.0 + bin: + analyze-trace: bin/analyze-trace + print-trace-types: bin/print-trace-types + simplify-trace-types: bin/simplify-trace-types + checksum: 967cad7eeedfd4c9e1a89f94a1613b81711237567a5f0061b242691a20dbe4d67b8dd5c50b4ec884165a324029b681529ff08edbcb8bf96b68e4cc5df52f1ce1 + languageName: node + linkType: hard + +"@ungap/structured-clone@npm:^1.2.0": + version: 1.2.0 + resolution: "@ungap/structured-clone@npm:1.2.0" + checksum: 4f656b7b4672f2ce6e272f2427d8b0824ed11546a601d8d5412b9d7704e83db38a8d9f402ecdf2b9063fc164af842ad0ec4a55819f621ed7e7ea4d1efcc74524 + languageName: node + linkType: hard + "@vitest/expect@npm:0.34.6": version: 0.34.6 resolution: "@vitest/expect@npm:0.34.6" @@ -902,10 +930,10 @@ __metadata: languageName: node linkType: hard -"abbrev@npm:^1.0.0": - version: 1.1.1 - resolution: "abbrev@npm:1.1.1" - checksum: a4a97ec07d7ea112c517036882b2ac22f3109b7b19077dc656316d07d308438aac28e4d9746dc4d84bf6b1e75b4a7b0a5f3cb30592419f128ca9a8cee3bcfa17 +"abbrev@npm:^2.0.0": + version: 2.0.0 + resolution: "abbrev@npm:2.0.0" + checksum: 0e994ad2aa6575f94670d8a2149afe94465de9cedaaaac364e7fb43a40c3691c980ff74899f682f4ca58fa96b4cbd7421a015d3a6defe43a442117d7821a2f36 languageName: node linkType: hard @@ -919,36 +947,27 @@ __metadata: linkType: hard "acorn-walk@npm:^8.2.0": - version: 8.2.0 - resolution: "acorn-walk@npm:8.2.0" - checksum: 1715e76c01dd7b2d4ca472f9c58968516a4899378a63ad5b6c2d668bba8da21a71976c14ec5f5b75f887b6317c4ae0b897ab141c831d741dc76024d8745f1ad1 + version: 8.3.0 + resolution: "acorn-walk@npm:8.3.0" + checksum: 15ea56ab6529135be05e7d018f935ca80a572355dd3f6d3cd717e36df3346e0f635a93ae781b1c7942607693e2e5f3ef81af5c6fc697bbadcc377ebda7b7f5f6 languageName: node linkType: hard "acorn@npm:^8.10.0, acorn@npm:^8.9.0": - version: 8.10.0 - resolution: "acorn@npm:8.10.0" + version: 8.11.2 + resolution: "acorn@npm:8.11.2" bin: acorn: bin/acorn - checksum: 538ba38af0cc9e5ef983aee196c4b8b4d87c0c94532334fa7e065b2c8a1f85863467bb774231aae91613fcda5e68740c15d97b1967ae3394d20faddddd8af61d - languageName: node - linkType: hard - -"agent-base@npm:6, agent-base@npm:^6.0.2": - version: 6.0.2 - resolution: "agent-base@npm:6.0.2" - dependencies: - debug: 4 - checksum: f52b6872cc96fd5f622071b71ef200e01c7c4c454ee68bc9accca90c98cfb39f2810e3e9aa330435835eedc8c23f4f8a15267f67c6e245d2b33757575bdac49d + checksum: 818450408684da89423e3daae24e4dc9b68692db8ab49ea4569c7c5abb7a3f23669438bf129cc81dfdada95e1c9b944ee1bfca2c57a05a4dc73834a612fbf6a7 languageName: node linkType: hard -"agentkeepalive@npm:^4.2.1": - version: 4.5.0 - resolution: "agentkeepalive@npm:4.5.0" +"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0": + version: 7.1.0 + resolution: "agent-base@npm:7.1.0" dependencies: - humanize-ms: ^1.2.1 - checksum: 13278cd5b125e51eddd5079f04d6fe0914ac1b8b91c1f3db2c1822f99ac1a7457869068997784342fe455d59daaff22e14fb7b8c3da4e741896e7e31faf92481 + debug: ^4.3.4 + checksum: f7828f991470a0cc22cb579c86a18cbae83d8a3cbed39992ab34fc7217c4d126017f1c74d0ab66be87f71455318a8ea3e757d6a37881b8d0f2a2c6aa55e5418f languageName: node linkType: hard @@ -1028,23 +1047,6 @@ __metadata: languageName: node linkType: hard -"aproba@npm:^1.0.3 || ^2.0.0": - version: 2.0.0 - resolution: "aproba@npm:2.0.0" - checksum: 5615cadcfb45289eea63f8afd064ab656006361020e1735112e346593856f87435e02d8dcc7ff0d11928bc7d425f27bc7c2a84f6c0b35ab0ff659c814c138a24 - languageName: node - linkType: hard - -"are-we-there-yet@npm:^3.0.0": - version: 3.0.1 - resolution: "are-we-there-yet@npm:3.0.1" - dependencies: - delegates: ^1.0.0 - readable-stream: ^3.6.0 - checksum: 52590c24860fa7173bedeb69a4c05fb573473e860197f618b9a28432ee4379049336727ae3a1f9c4cb083114601c1140cee578376164d0e651217a9843f9fe83 - languageName: node - linkType: hard - "argparse@npm:^2.0.1": version: 2.0.1 resolution: "argparse@npm:2.0.1" @@ -1217,14 +1219,14 @@ __metadata: languageName: node linkType: hard -"cacache@npm:^17.0.0": - version: 17.1.4 - resolution: "cacache@npm:17.1.4" +"cacache@npm:^18.0.0": + version: 18.0.0 + resolution: "cacache@npm:18.0.0" dependencies: "@npmcli/fs": ^3.1.0 fs-minipass: ^3.0.0 glob: ^10.2.2 - lru-cache: ^7.7.1 + lru-cache: ^10.0.1 minipass: ^7.0.3 minipass-collect: ^1.0.2 minipass-flush: ^1.0.5 @@ -1233,17 +1235,18 @@ __metadata: ssri: ^10.0.0 tar: ^6.1.11 unique-filename: ^3.0.0 - checksum: b7751df756656954a51201335addced8f63fc53266fa56392c9f5ae83c8d27debffb4458ac2d168a744a4517ec3f2163af05c20097f93d17bdc2dc8a385e14a6 + checksum: 2cd6bf15551abd4165acb3a4d1ef0593b3aa2fd6853ae16b5bb62199c2faecf27d36555a9545c0e07dd03347ec052e782923bdcece724a24611986aafb53e152 languageName: node linkType: hard -"call-bind@npm:^1.0.0, call-bind@npm:^1.0.2": - version: 1.0.2 - resolution: "call-bind@npm:1.0.2" +"call-bind@npm:^1.0.0, call-bind@npm:^1.0.2, call-bind@npm:^1.0.4, call-bind@npm:^1.0.5": + version: 1.0.5 + resolution: "call-bind@npm:1.0.5" dependencies: - function-bind: ^1.1.1 - get-intrinsic: ^1.0.2 - checksum: f8e31de9d19988a4b80f3e704788c4a2d6b6f3d17cfec4f57dc29ced450c53a49270dc66bf0fbd693329ee948dd33e6c90a329519aef17474a4d961e8d6426b0 + function-bind: ^1.1.2 + get-intrinsic: ^1.2.1 + set-function-length: ^1.1.1 + checksum: 449e83ecbd4ba48e7eaac5af26fea3b50f8f6072202c2dd7c5a6e7a6308f2421abe5e13a3bbd55221087f76320c5e09f25a8fdad1bab2b77c68ae74d92234ea5 languageName: node linkType: hard @@ -1269,7 +1272,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4.0.0": +"chalk@npm:^4.0.0, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -1321,6 +1324,17 @@ __metadata: languageName: node linkType: hard +"cliui@npm:^7.0.2": + version: 7.0.4 + resolution: "cliui@npm:7.0.4" + dependencies: + string-width: ^4.2.0 + strip-ansi: ^6.0.0 + wrap-ansi: ^7.0.0 + checksum: ce2e8f578a4813806788ac399b9e866297740eecd4ad1823c27fd344d78b22c5f8597d548adbcc46f0573e43e21e751f39446c5a5e804a12aace402b7a315d7f + languageName: node + linkType: hard + "color-convert@npm:^2.0.1": version: 2.0.1 resolution: "color-convert@npm:2.0.1" @@ -1337,15 +1351,6 @@ __metadata: languageName: node linkType: hard -"color-support@npm:^1.1.3": - version: 1.1.3 - resolution: "color-support@npm:1.1.3" - bin: - color-support: bin.js - checksum: 9b7356817670b9a13a26ca5af1c21615463b500783b739b7634a0c2047c16cef4b2865d7576875c31c3cddf9dd621fa19285e628f20198b233a5cfdda6d0793b - languageName: node - linkType: hard - "commander@npm:^4.0.0": version: 4.1.1 resolution: "commander@npm:4.1.1" @@ -1360,13 +1365,6 @@ __metadata: languageName: node linkType: hard -"console-control-strings@npm:^1.1.0": - version: 1.1.0 - resolution: "console-control-strings@npm:1.1.0" - checksum: 8755d76787f94e6cf79ce4666f0c5519906d7f5b02d4b884cf41e11dcd759ed69c57da0670afd9236d229a46e0f9cf519db0cd829c6dca820bb5a5c3def584ed - languageName: node - linkType: hard - "cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" @@ -1385,7 +1383,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": +"debug@npm:4, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -1413,14 +1411,14 @@ __metadata: languageName: node linkType: hard -"define-data-property@npm:^1.0.1": - version: 1.1.0 - resolution: "define-data-property@npm:1.1.0" +"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.1": + version: 1.1.1 + resolution: "define-data-property@npm:1.1.1" dependencies: get-intrinsic: ^1.2.1 gopd: ^1.0.1 has-property-descriptors: ^1.0.0 - checksum: 7ad4ee84cca8ad427a4831f5693526804b62ce9dfd4efac77214e95a4382aed930072251d4075dc8dc9fc949a353ed51f19f5285a84a788ba9216cc51472a093 + checksum: a29855ad3f0630ea82e3c5012c812efa6ca3078d5c2aa8df06b5f597c1cde6f7254692df41945851d903e05a1668607b6d34e778f402b9ff9ffb38111f1a3f0d languageName: node linkType: hard @@ -1435,13 +1433,6 @@ __metadata: languageName: node linkType: hard -"delegates@npm:^1.0.0": - version: 1.0.0 - resolution: "delegates@npm:1.0.0" - checksum: a51744d9b53c164ba9c0492471a1a2ffa0b6727451bdc89e31627fdf4adda9d51277cfcbfb20f0a6f08ccb3c436f341df3e92631a3440226d93a8971724771fd - languageName: node - linkType: hard - "diff-sequences@npm:^29.4.3": version: 29.6.3 resolution: "diff-sequences@npm:29.6.3" @@ -1521,24 +1512,24 @@ __metadata: linkType: hard "es-abstract@npm:^1.22.1": - version: 1.22.2 - resolution: "es-abstract@npm:1.22.2" + version: 1.22.3 + resolution: "es-abstract@npm:1.22.3" dependencies: array-buffer-byte-length: ^1.0.0 arraybuffer.prototype.slice: ^1.0.2 available-typed-arrays: ^1.0.5 - call-bind: ^1.0.2 + call-bind: ^1.0.5 es-set-tostringtag: ^2.0.1 es-to-primitive: ^1.2.1 function.prototype.name: ^1.1.6 - get-intrinsic: ^1.2.1 + get-intrinsic: ^1.2.2 get-symbol-description: ^1.0.0 globalthis: ^1.0.3 gopd: ^1.0.1 - has: ^1.0.3 has-property-descriptors: ^1.0.0 has-proto: ^1.0.1 has-symbols: ^1.0.3 + hasown: ^2.0.0 internal-slot: ^1.0.5 is-array-buffer: ^3.0.2 is-callable: ^1.2.7 @@ -1548,7 +1539,7 @@ __metadata: is-string: ^1.0.7 is-typed-array: ^1.1.12 is-weakref: ^1.0.2 - object-inspect: ^1.12.3 + object-inspect: ^1.13.1 object-keys: ^1.1.1 object.assign: ^4.1.4 regexp.prototype.flags: ^1.5.1 @@ -1562,8 +1553,8 @@ __metadata: typed-array-byte-offset: ^1.0.0 typed-array-length: ^1.0.4 unbox-primitive: ^1.0.2 - which-typed-array: ^1.1.11 - checksum: cc70e592d360d7d729859013dee7a610c6b27ed8630df0547c16b0d16d9fe6505a70ee14d1af08d970fdd132b3f88c9ca7815ce72c9011608abf8ab0e55fc515 + which-typed-array: ^1.1.13 + checksum: b1bdc962856836f6e72be10b58dc128282bdf33771c7a38ae90419d920fc3b36cc5d2b70a222ad8016e3fc322c367bf4e9e89fc2bc79b7e933c05b218e83d79a languageName: node linkType: hard @@ -1590,22 +1581,22 @@ __metadata: linkType: hard "es-set-tostringtag@npm:^2.0.1": - version: 2.0.1 - resolution: "es-set-tostringtag@npm:2.0.1" + version: 2.0.2 + resolution: "es-set-tostringtag@npm:2.0.2" dependencies: - get-intrinsic: ^1.1.3 - has: ^1.0.3 + get-intrinsic: ^1.2.2 has-tostringtag: ^1.0.0 - checksum: ec416a12948cefb4b2a5932e62093a7cf36ddc3efd58d6c58ca7ae7064475ace556434b869b0bbeb0c365f1032a8ccd577211101234b69837ad83ad204fff884 + hasown: ^2.0.0 + checksum: afcec3a4c9890ae14d7ec606204858441c801ff84f312538e1d1ccf1e5493c8b17bd672235df785f803756472cb4f2d49b87bde5237aef33411e74c22f194e07 languageName: node linkType: hard "es-shim-unscopables@npm:^1.0.0": - version: 1.0.0 - resolution: "es-shim-unscopables@npm:1.0.0" + version: 1.0.2 + resolution: "es-shim-unscopables@npm:1.0.2" dependencies: - has: ^1.0.3 - checksum: 83e95cadbb6ee44d3644dfad60dcad7929edbc42c85e66c3e99aefd68a3a5c5665f2686885cddb47dfeabfd77bd5ea5a7060f2092a955a729bbd8834f0d86fa1 + hasown: ^2.0.0 + checksum: 432bd527c62065da09ed1d37a3f8e623c423683285e6188108286f4a1e8e164a5bcbfbc0051557c7d14633cd2a41ce24c7048e6bbb66a985413fd32f1be72626 languageName: node linkType: hard @@ -1774,6 +1765,13 @@ __metadata: languageName: node linkType: hard +"escalade@npm:^3.1.1": + version: 3.1.1 + resolution: "escalade@npm:3.1.1" + checksum: a3e2a99f07acb74b3ad4989c48ca0c3140f69f923e56d0cba0526240ee470b91010f9d39001f2a4a313841d237ede70a729e92125191ba5d21e74b106800b133 + languageName: node + linkType: hard + "escape-string-regexp@npm:^4.0.0": version: 4.0.0 resolution: "escape-string-regexp@npm:4.0.0" @@ -1862,16 +1860,17 @@ __metadata: linkType: hard "eslint@npm:^8.0.1": - version: 8.50.0 - resolution: "eslint@npm:8.50.0" + version: 8.53.0 + resolution: "eslint@npm:8.53.0" dependencies: "@eslint-community/eslint-utils": ^4.2.0 "@eslint-community/regexpp": ^4.6.1 - "@eslint/eslintrc": ^2.1.2 - "@eslint/js": 8.50.0 - "@humanwhocodes/config-array": ^0.11.11 + "@eslint/eslintrc": ^2.1.3 + "@eslint/js": 8.53.0 + "@humanwhocodes/config-array": ^0.11.13 "@humanwhocodes/module-importer": ^1.0.1 "@nodelib/fs.walk": ^1.2.8 + "@ungap/structured-clone": ^1.2.0 ajv: ^6.12.4 chalk: ^4.0.0 cross-spawn: ^7.0.2 @@ -1904,7 +1903,7 @@ __metadata: text-table: ^0.2.0 bin: eslint: bin/eslint.js - checksum: 9ebfe5615dc84700000d218e32ddfdcfc227ca600f65f18e5541ec34f8902a00356a9a8804d9468fd6c8637a5ef6a3897291dad91ba6579d5b32ffeae5e31768 + checksum: 2da808655c7aa4b33f8970ba30d96b453c3071cc4d6cd60d367163430677e32ff186b65270816b662d29139283138bff81f28dddeb2e73265495245a316ed02c languageName: node linkType: hard @@ -1975,6 +1974,13 @@ __metadata: languageName: node linkType: hard +"exit@npm:^0.1.2": + version: 0.1.2 + resolution: "exit@npm:0.1.2" + checksum: abc407f07a875c3961e4781dfcb743b58d6c93de9ab263f4f8c9d23bb6da5f9b7764fc773f86b43dd88030444d5ab8abcb611cb680fba8ca075362b77114bba3 + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" @@ -1990,15 +1996,15 @@ __metadata: linkType: hard "fast-glob@npm:^3.2.9": - version: 3.3.1 - resolution: "fast-glob@npm:3.3.1" + version: 3.3.2 + resolution: "fast-glob@npm:3.3.2" dependencies: "@nodelib/fs.stat": ^2.0.2 "@nodelib/fs.walk": ^1.2.3 glob-parent: ^5.1.2 merge2: ^1.3.0 micromatch: ^4.0.4 - checksum: b6f3add6403e02cf3a798bfbb1183d0f6da2afd368f27456010c0bc1f9640aea308243d4cb2c0ab142f618276e65ecb8be1661d7c62a7b4e5ba774b9ce5432e5 + checksum: 900e4979f4dbc3313840078419245621259f349950411ca2fa445a2f9a1a6d98c3b5e7e0660c5ccd563aa61abe133a21765c6c0dec8e57da1ba71d8000b05ec1 languageName: node linkType: hard @@ -2054,17 +2060,17 @@ __metadata: linkType: hard "flat-cache@npm:^3.0.4": - version: 3.1.0 - resolution: "flat-cache@npm:3.1.0" + version: 3.2.0 + resolution: "flat-cache@npm:3.2.0" dependencies: - flatted: ^3.2.7 + flatted: ^3.2.9 keyv: ^4.5.3 rimraf: ^3.0.2 - checksum: 99312601d5b90f44aef403f17f056dc09be7e437703740b166cdc9386d99e681f74e6b6e8bd7d010bda66904ea643c9527276b1b80308a2119741d94108a4d8f + checksum: e7e0f59801e288b54bee5cb9681e9ee21ee28ef309f886b312c9d08415b79fc0f24ac842f84356ce80f47d6a53de62197ce0e6e148dc42d5db005992e2a756ec languageName: node linkType: hard -"flatted@npm:^3.2.7": +"flatted@npm:^3.2.9": version: 3.2.9 resolution: "flatted@npm:3.2.9" checksum: f14167fbe26a9d20f6fca8d998e8f1f41df72c8e81f9f2c9d61ed2bea058248f5e1cbd05e7f88c0e5087a6a0b822a1e5e2b446e879f3cfbe0b07ba2d7f80b026 @@ -2134,10 +2140,10 @@ __metadata: languageName: node linkType: hard -"function-bind@npm:^1.1.1": - version: 1.1.1 - resolution: "function-bind@npm:1.1.1" - checksum: b32fbaebb3f8ec4969f033073b43f5c8befbb58f1a79e12f1d7490358150359ebd92f49e72ff0144f65f2c48ea2a605bff2d07965f548f6474fd8efd95bf361a +"function-bind@npm:^1.1.1, function-bind@npm:^1.1.2": + version: 1.1.2 + resolution: "function-bind@npm:1.1.2" + checksum: 2b0ff4ce708d99715ad14a6d1f894e2a83242e4a52ccfcefaee5e40050562e5f6dafc1adbb4ce2d4ab47279a45dc736ab91ea5042d843c3c092820dfe032efb1 languageName: node linkType: hard @@ -2167,38 +2173,29 @@ __metadata: languageName: node linkType: hard -"gauge@npm:^4.0.3": - version: 4.0.4 - resolution: "gauge@npm:4.0.4" - dependencies: - aproba: ^1.0.3 || ^2.0.0 - color-support: ^1.1.3 - console-control-strings: ^1.1.0 - has-unicode: ^2.0.1 - signal-exit: ^3.0.7 - string-width: ^4.2.3 - strip-ansi: ^6.0.1 - wide-align: ^1.1.5 - checksum: 788b6bfe52f1dd8e263cda800c26ac0ca2ff6de0b6eee2fe0d9e3abf15e149b651bd27bf5226be10e6e3edb5c4e5d5985a5a1a98137e7a892f75eff76467ad2d +"get-caller-file@npm:^2.0.5": + version: 2.0.5 + resolution: "get-caller-file@npm:2.0.5" + checksum: b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9 languageName: node linkType: hard -"get-func-name@npm:^2.0.0, get-func-name@npm:^2.0.2": +"get-func-name@npm:^2.0.1, get-func-name@npm:^2.0.2": version: 2.0.2 resolution: "get-func-name@npm:2.0.2" checksum: 3f62f4c23647de9d46e6f76d2b3eafe58933a9b3830c60669e4180d6c601ce1b4aa310ba8366143f55e52b139f992087a9f0647274e8745621fa2af7e0acf13b languageName: node linkType: hard -"get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.0, get-intrinsic@npm:^1.2.1": - version: 1.2.1 - resolution: "get-intrinsic@npm:1.2.1" +"get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.0, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.2": + version: 1.2.2 + resolution: "get-intrinsic@npm:1.2.2" dependencies: - function-bind: ^1.1.1 - has: ^1.0.3 + function-bind: ^1.1.2 has-proto: ^1.0.1 has-symbols: ^1.0.3 - checksum: 5b61d88552c24b0cf6fa2d1b3bc5459d7306f699de060d76442cce49a4721f52b8c560a33ab392cf5575b7810277d54ded9d4d39a1ea61855619ebc005aa7e5f + hasown: ^2.0.0 + checksum: 447ff0724df26829908dc033b62732359596fcf66027bc131ab37984afb33842d9cd458fd6cecadfe7eac22fd8a54b349799ed334cf2726025c921c7250e7417 languageName: node linkType: hard @@ -2251,7 +2248,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.2.2": +"glob@npm:^10.2.2, glob@npm:^10.3.10": version: 10.3.10 resolution: "glob@npm:10.3.10" dependencies: @@ -2266,7 +2263,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.0.0, glob@npm:^7.1.3, glob@npm:^7.1.4": +"glob@npm:^7.0.0, glob@npm:^7.1.3": version: 7.2.3 resolution: "glob@npm:7.2.3" dependencies: @@ -2281,11 +2278,11 @@ __metadata: linkType: hard "globals@npm:^13.19.0": - version: 13.22.0 - resolution: "globals@npm:13.22.0" + version: 13.23.0 + resolution: "globals@npm:13.23.0" dependencies: type-fest: ^0.20.2 - checksum: 64af5a09565341432770444085f7aa98b54331c3b69732e0de411003921fa2dd060222ae7b50bec0b98f29c4d00b4f49bf434049ba9f7c36ca4ee1773f60458c + checksum: 194c97cf8d1ef6ba59417234c2386549c4103b6e5f24b1ff1952de61a4753e5d2069435ba629de711a6480b1b1d114a98e2ab27f85e966d5a10c319c3bbd3dc3 languageName: node linkType: hard @@ -2350,11 +2347,11 @@ __metadata: linkType: hard "has-property-descriptors@npm:^1.0.0": - version: 1.0.0 - resolution: "has-property-descriptors@npm:1.0.0" + version: 1.0.1 + resolution: "has-property-descriptors@npm:1.0.1" dependencies: - get-intrinsic: ^1.1.1 - checksum: a6d3f0a266d0294d972e354782e872e2fe1b6495b321e6ef678c9b7a06a40408a6891817350c62e752adced73a94ac903c54734fee05bf65b1905ee1368194bb + get-intrinsic: ^1.2.2 + checksum: 2bcc6bf6ec6af375add4e4b4ef586e43674850a91ad4d46666d0b28ba8e1fd69e424c7677d24d60f69470ad0afaa2f3197f508b20b0bb7dd99a8ab77ffc4b7c4 languageName: node linkType: hard @@ -2381,19 +2378,12 @@ __metadata: languageName: node linkType: hard -"has-unicode@npm:^2.0.1": - version: 2.0.1 - resolution: "has-unicode@npm:2.0.1" - checksum: 1eab07a7436512db0be40a710b29b5dc21fa04880b7f63c9980b706683127e3c1b57cb80ea96d47991bdae2dfe479604f6a1ba410106ee1046a41d1bd0814400 - languageName: node - linkType: hard - -"has@npm:^1.0.3": - version: 1.0.3 - resolution: "has@npm:1.0.3" +"hasown@npm:^2.0.0": + version: 2.0.0 + resolution: "hasown@npm:2.0.0" dependencies: - function-bind: ^1.1.1 - checksum: b9ad53d53be4af90ce5d1c38331e712522417d017d5ef1ebd0507e07c2fbad8686fffb8e12ddecd4c39ca9b9b47431afbb975b8abf7f3c3b82c98e9aad052792 + function-bind: ^1.1.2 + checksum: 6151c75ca12554565098641c98a40f4cc86b85b0fd5b6fe92360967e4605a4f9610f7757260b4e8098dd1c2ce7f4b095f2006fe72a570e3b6d2d28de0298c176 languageName: node linkType: hard @@ -2413,24 +2403,23 @@ __metadata: languageName: node linkType: hard -"http-proxy-agent@npm:^5.0.0": - version: 5.0.0 - resolution: "http-proxy-agent@npm:5.0.0" +"http-proxy-agent@npm:^7.0.0": + version: 7.0.0 + resolution: "http-proxy-agent@npm:7.0.0" dependencies: - "@tootallnate/once": 2 - agent-base: 6 - debug: 4 - checksum: e2ee1ff1656a131953839b2a19cd1f3a52d97c25ba87bd2559af6ae87114abf60971e498021f9b73f9fd78aea8876d1fb0d4656aac8a03c6caa9fc175f22b786 + agent-base: ^7.1.0 + debug: ^4.3.4 + checksum: 48d4fac997917e15f45094852b63b62a46d0c8a4f0b9c6c23ca26d27b8df8d178bed88389e604745e748bd9a01f5023e25093722777f0593c3f052009ff438b6 languageName: node linkType: hard -"https-proxy-agent@npm:^5.0.0": - version: 5.0.1 - resolution: "https-proxy-agent@npm:5.0.1" +"https-proxy-agent@npm:^7.0.1": + version: 7.0.2 + resolution: "https-proxy-agent@npm:7.0.2" dependencies: - agent-base: 6 + agent-base: ^7.0.2 debug: 4 - checksum: 571fccdf38184f05943e12d37d6ce38197becdd69e58d03f43637f7fa1269cf303a7d228aa27e5b27bbd3af8f09fd938e1c91dcfefff2df7ba77c20ed8dfc765 + checksum: 088969a0dd476ea7a0ed0a2cf1283013682b08f874c3bc6696c83fa061d2c157d29ef0ad3eb70a2046010bb7665573b2388d10fdcb3e410a66995e5248444292 languageName: node linkType: hard @@ -2441,15 +2430,6 @@ __metadata: languageName: node linkType: hard -"humanize-ms@npm:^1.2.1": - version: 1.2.1 - resolution: "humanize-ms@npm:1.2.1" - dependencies: - ms: ^2.0.0 - checksum: 9c7a74a2827f9294c009266c82031030eae811ca87b0da3dceb8d6071b9bde22c9f3daef0469c3c533cc67a97d8a167cd9fc0389350e5f415f61a79b171ded16 - languageName: node - linkType: hard - "iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" @@ -2515,13 +2495,13 @@ __metadata: linkType: hard "internal-slot@npm:^1.0.5": - version: 1.0.5 - resolution: "internal-slot@npm:1.0.5" + version: 1.0.6 + resolution: "internal-slot@npm:1.0.6" dependencies: - get-intrinsic: ^1.2.0 - has: ^1.0.3 + get-intrinsic: ^1.2.2 + hasown: ^2.0.0 side-channel: ^1.0.4 - checksum: 97e84046bf9e7574d0956bd98d7162313ce7057883b6db6c5c7b5e5f05688864b0978ba07610c726d15d66544ffe4b1050107d93f8a39ebc59b15d8b429b497a + checksum: 7872454888047553ce97a3fa1da7cc054a28ec5400a9c2e9f4dbe4fe7c1d041cb8e8301467614b80d4246d50377aad2fb58860b294ed74d6700cc346b6f89549 languageName: node linkType: hard @@ -2594,12 +2574,12 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.13.0, is-core-module@npm:^2.9.0": - version: 2.13.0 - resolution: "is-core-module@npm:2.13.0" +"is-core-module@npm:^2.13.0": + version: 2.13.1 + resolution: "is-core-module@npm:2.13.1" dependencies: - has: ^1.0.3 - checksum: 053ab101fb390bfeb2333360fd131387bed54e476b26860dc7f5a700bbf34a0ec4454f7c8c4d43e8a0030957e4b3db6e16d35e1890ea6fb654c833095e040355 + hasown: ^2.0.0 + checksum: 256559ee8a9488af90e4bad16f5583c6d59e92f0742e9e8bb4331e758521ee86b810b93bae44f390766ffbc518a0488b18d9dab7da9a5ff997d499efc9403f7c languageName: node linkType: hard @@ -2797,6 +2777,13 @@ __metadata: languageName: node linkType: hard +"isexe@npm:^3.1.1": + version: 3.1.1 + resolution: "isexe@npm:3.1.1" + checksum: 7fe1931ee4e88eb5aa524cd3ceb8c882537bc3a81b02e438b240e47012eef49c86904d0f0e593ea7c3a9996d18d0f1f3be8d3eaa92333977b0c3a9d353d5563e + languageName: node + linkType: hard + "iterator.prototype@npm:^1.1.2": version: 1.1.2 resolution: "iterator.prototype@npm:1.1.2" @@ -2876,6 +2863,25 @@ __metadata: languageName: node linkType: hard +"jsonparse@npm:^1.2.0, jsonparse@npm:^1.3.1": + version: 1.3.1 + resolution: "jsonparse@npm:1.3.1" + checksum: 6514a7be4674ebf407afca0eda3ba284b69b07f9958a8d3113ef1005f7ec610860c312be067e450c569aab8b89635e332cee3696789c750692bb60daba627f4d + languageName: node + linkType: hard + +"jsonstream-next@npm:^3.0.0": + version: 3.0.0 + resolution: "jsonstream-next@npm:3.0.0" + dependencies: + jsonparse: ^1.2.0 + through2: ^4.0.2 + bin: + jsonstream-next: bin.js + checksum: 651d9d304ae9b23e397f0c1c60d8679daab41f42981eca6eaa61527a2f249d1cc0e8f3b3da2ce686590933ee92301d2092cb4cc65a24206d5a4e4409b77bdc21 + languageName: node + linkType: hard + "jsx-ast-utils@npm:^2.4.1 || ^3.0.0": version: 3.3.5 resolution: "jsx-ast-utils@npm:3.3.5" @@ -2889,11 +2895,11 @@ __metadata: linkType: hard "keyv@npm:^4.5.3": - version: 4.5.3 - resolution: "keyv@npm:4.5.3" + version: 4.5.4 + resolution: "keyv@npm:4.5.4" dependencies: json-buffer: 3.0.1 - checksum: 3ffb4d5b72b6b4b4af443bbb75ca2526b23c750fccb5ac4c267c6116888b4b65681015c2833cb20d26cf3e6e32dac6b988c77f7f022e1a571b7d90f1442257da + checksum: 74a24395b1c34bd44ad5cb2b49140d087553e170625240b86755a6604cd65aa16efdbdeae5cdb17ba1284a0fbb25ad06263755dbc71b8d8b06f74232ce3cdd72 languageName: node linkType: hard @@ -2984,11 +2990,20 @@ __metadata: linkType: hard "loupe@npm:^2.3.6": - version: 2.3.6 - resolution: "loupe@npm:2.3.6" + version: 2.3.7 + resolution: "loupe@npm:2.3.7" + dependencies: + get-func-name: ^2.0.1 + checksum: 96c058ec7167598e238bb7fb9def2f9339215e97d6685d9c1e3e4bdb33d14600e11fe7a812cf0c003dfb73ca2df374f146280b2287cae9e8d989e9d7a69a203b + languageName: node + linkType: hard + +"lru-cache@npm:^10.0.1, lru-cache@npm:^9.1.1 || ^10.0.0": + version: 10.0.2 + resolution: "lru-cache@npm:10.0.2" dependencies: - get-func-name: ^2.0.0 - checksum: cc83f1b124a1df7384601d72d8d1f5fe95fd7a8185469fec48bb2e4027e45243949e7a013e8d91051a138451ff0552310c32aa9786e60b6a30d1e801bdc2163f + semver: ^7.3.5 + checksum: 83ad0e899d79f48574bdda131fe8157c6d65cbd073a6e78e0d1a3467a85dce1ef4d8dc9fd618a56c57a068271501c81d54471e13f84dd121e046b155ed061ed4 languageName: node linkType: hard @@ -3001,20 +3016,6 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^7.7.1": - version: 7.18.3 - resolution: "lru-cache@npm:7.18.3" - checksum: e550d772384709deea3f141af34b6d4fa392e2e418c1498c078de0ee63670f1f46f5eee746e8ef7e69e1c895af0d4224e62ee33e66a543a14763b0f2e74c1356 - languageName: node - linkType: hard - -"lru-cache@npm:^9.1.1 || ^10.0.0": - version: 10.0.1 - resolution: "lru-cache@npm:10.0.1" - checksum: 06f8d0e1ceabd76bb6f644a26dbb0b4c471b79c7b514c13c6856113879b3bf369eb7b497dad4ff2b7e2636db202412394865b33c332100876d838ad1372f0181 - languageName: node - linkType: hard - "magic-string@npm:^0.30.1": version: 0.30.5 resolution: "magic-string@npm:0.30.5" @@ -3024,26 +3025,22 @@ __metadata: languageName: node linkType: hard -"make-fetch-happen@npm:^11.0.3": - version: 11.1.1 - resolution: "make-fetch-happen@npm:11.1.1" +"make-fetch-happen@npm:^13.0.0": + version: 13.0.0 + resolution: "make-fetch-happen@npm:13.0.0" dependencies: - agentkeepalive: ^4.2.1 - cacache: ^17.0.0 + "@npmcli/agent": ^2.0.0 + cacache: ^18.0.0 http-cache-semantics: ^4.1.1 - http-proxy-agent: ^5.0.0 - https-proxy-agent: ^5.0.0 is-lambda: ^1.0.1 - lru-cache: ^7.7.1 - minipass: ^5.0.0 + minipass: ^7.0.2 minipass-fetch: ^3.0.0 minipass-flush: ^1.0.5 minipass-pipeline: ^1.2.4 negotiator: ^0.6.3 promise-retry: ^2.0.1 - socks-proxy-agent: ^7.0.0 ssri: ^10.0.0 - checksum: 7268bf274a0f6dcf0343829489a4506603ff34bd0649c12058753900b0eb29191dce5dba12680719a5d0a983d3e57810f594a12f3c18494e93a1fbc6348a4540 + checksum: 7c7a6d381ce919dd83af398b66459a10e2fe8f4504f340d1d090d3fa3d1b0c93750220e1d898114c64467223504bd258612ba83efbc16f31b075cd56de24b4af languageName: node linkType: hard @@ -3177,7 +3174,7 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.3": +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3": version: 7.0.4 resolution: "minipass@npm:7.0.4" checksum: 87585e258b9488caf2e7acea242fd7856bbe9a2c84a7807643513a338d66f368c7d518200ad7b70a508664d408aa000517647b2930c259a8b1f9f0984f344a21 @@ -3222,13 +3219,6 @@ __metadata: languageName: node linkType: hard -"ms@npm:^2.0.0": - version: 2.1.3 - resolution: "ms@npm:2.1.3" - checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d - languageName: node - linkType: hard - "mz@npm:^2.7.0": version: 2.7.0 resolution: "mz@npm:2.7.0" @@ -3241,11 +3231,11 @@ __metadata: linkType: hard "nanoid@npm:^3.3.6": - version: 3.3.6 - resolution: "nanoid@npm:3.3.6" + version: 3.3.7 + resolution: "nanoid@npm:3.3.7" bin: nanoid: bin/nanoid.cjs - checksum: 7d0eda657002738aa5206107bd0580aead6c95c460ef1bdd0b1a87a9c7ae6277ac2e9b945306aaa5b32c6dcb7feaf462d0f552e7f8b5718abfc6ead5c94a71b3 + checksum: d36c427e530713e4ac6567d488b489a36582ef89da1d6d4e3b87eded11eb10d7042a877958c6f104929809b2ab0bafa17652b076cdf84324aa75b30b722204f2 languageName: node linkType: hard @@ -3264,34 +3254,33 @@ __metadata: linkType: hard "node-gyp@npm:latest": - version: 9.4.0 - resolution: "node-gyp@npm:9.4.0" + version: 10.0.1 + resolution: "node-gyp@npm:10.0.1" dependencies: env-paths: ^2.2.0 exponential-backoff: ^3.1.1 - glob: ^7.1.4 + glob: ^10.3.10 graceful-fs: ^4.2.6 - make-fetch-happen: ^11.0.3 - nopt: ^6.0.0 - npmlog: ^6.0.0 - rimraf: ^3.0.2 + make-fetch-happen: ^13.0.0 + nopt: ^7.0.0 + proc-log: ^3.0.0 semver: ^7.3.5 tar: ^6.1.2 - which: ^2.0.2 + which: ^4.0.0 bin: node-gyp: bin/node-gyp.js - checksum: 78b404e2e0639d64e145845f7f5a3cb20c0520cdaf6dda2f6e025e9b644077202ea7de1232396ba5bde3fee84cdc79604feebe6ba3ec84d464c85d407bb5da99 + checksum: 60a74e66d364903ce02049966303a57f898521d139860ac82744a5fdd9f7b7b3b61f75f284f3bfe6e6add3b8f1871ce305a1d41f775c7482de837b50c792223f languageName: node linkType: hard -"nopt@npm:^6.0.0": - version: 6.0.0 - resolution: "nopt@npm:6.0.0" +"nopt@npm:^7.0.0": + version: 7.2.0 + resolution: "nopt@npm:7.2.0" dependencies: - abbrev: ^1.0.0 + abbrev: ^2.0.0 bin: nopt: bin/nopt.js - checksum: 82149371f8be0c4b9ec2f863cc6509a7fd0fa729929c009f3a58e4eb0c9e4cae9920e8f1f8eb46e7d032fec8fb01bede7f0f41a67eb3553b7b8e14fa53de1dac + checksum: a9c0f57fb8cb9cc82ae47192ca2b7ef00e199b9480eed202482c962d61b59a7fbe7541920b2a5839a97b42ee39e288c0aed770e38057a608d7f579389dfde410 languageName: node linkType: hard @@ -3311,18 +3300,6 @@ __metadata: languageName: node linkType: hard -"npmlog@npm:^6.0.0": - version: 6.0.2 - resolution: "npmlog@npm:6.0.2" - dependencies: - are-we-there-yet: ^3.0.0 - console-control-strings: ^1.1.0 - gauge: ^4.0.3 - set-blocking: ^2.0.0 - checksum: ae238cd264a1c3f22091cdd9e2b106f684297d3c184f1146984ecbe18aaa86343953f26b9520dedd1b1372bc0316905b736c1932d778dbeb1fcf5a1001390e2a - languageName: node - linkType: hard - "object-assign@npm:^4.0.1, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" @@ -3330,10 +3307,10 @@ __metadata: languageName: node linkType: hard -"object-inspect@npm:^1.12.3, object-inspect@npm:^1.9.0": - version: 1.12.3 - resolution: "object-inspect@npm:1.12.3" - checksum: dabfd824d97a5f407e6d5d24810d888859f6be394d8b733a77442b277e0808860555176719c5905e765e3743a7cada6b8b0a3b85e5331c530fd418cc8ae991db +"object-inspect@npm:^1.13.1, object-inspect@npm:^1.9.0": + version: 1.13.1 + resolution: "object-inspect@npm:1.13.1" + checksum: 7d9fa9221de3311dcb5c7c307ee5dc011cdd31dc43624b7c184b3840514e118e05ef0002be5388304c416c0eb592feb46e983db12577fc47e47d5752fbbfb61f languageName: node linkType: hard @@ -3431,7 +3408,7 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^3.0.2": +"p-limit@npm:^3.0.2, p-limit@npm:^3.1.0": version: 3.1.0 resolution: "p-limit@npm:3.1.0" dependencies: @@ -3623,6 +3600,13 @@ __metadata: languageName: node linkType: hard +"proc-log@npm:^3.0.0": + version: 3.0.0 + resolution: "proc-log@npm:3.0.0" + checksum: 02b64e1b3919e63df06f836b98d3af002b5cd92655cab18b5746e37374bfb73e03b84fe305454614b34c25b485cc687a9eebdccf0242cda8fda2475dd2c97e02 + languageName: node + linkType: hard + "promise-retry@npm:^2.0.1": version: 2.0.1 resolution: "promise-retry@npm:2.0.1" @@ -3645,9 +3629,9 @@ __metadata: linkType: hard "punycode@npm:^2.1.0": - version: 2.3.0 - resolution: "punycode@npm:2.3.0" - checksum: 39f760e09a2a3bbfe8f5287cf733ecdad69d6af2fe6f97ca95f24b8921858b91e9ea3c9eeec6e08cede96181b3bb33f95c6ffd8c77e63986508aa2e8159fa200 + version: 2.3.1 + resolution: "punycode@npm:2.3.1" + checksum: bb0a0ceedca4c3c57a9b981b90601579058903c62be23c5e8e843d2c2d4148a3ecf029d5133486fb0e1822b098ba8bba09e89d6b21742d02fa26bda6441a6fb2 languageName: node linkType: hard @@ -3725,7 +3709,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^3.6.0": +"readable-stream@npm:3, readable-stream@npm:^3.0.0": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" dependencies: @@ -3811,6 +3795,13 @@ __metadata: languageName: node linkType: hard +"require-directory@npm:^2.1.1": + version: 2.1.1 + resolution: "require-directory@npm:2.1.1" + checksum: fb47e70bf0001fdeabdc0429d431863e9475e7e43ea5f94ad86503d918423c1543361cc5166d713eaa7029dd7a3d34775af04764bebff99ef413111a5af18c80 + languageName: node + linkType: hard + "requireindex@npm:~1.1.0": version: 1.1.0 resolution: "requireindex@npm:1.1.0" @@ -3835,6 +3826,7 @@ __metadata: "@typescript-eslint/eslint-plugin": 5.1.0 "@typescript-eslint/eslint-plugin-tslint": 5.1.0 "@typescript-eslint/parser": 5.1.0 + "@typescript/analyze-trace": ^0.10.1 eslint: ^8.0.1 eslint-plugin-react: ^7.26.1 eslint-plugin-typescript: 0.14.0 @@ -3868,54 +3860,54 @@ __metadata: linkType: hard "resolve@npm:^1.1.6": - version: 1.22.6 - resolution: "resolve@npm:1.22.6" + version: 1.22.8 + resolution: "resolve@npm:1.22.8" dependencies: is-core-module: ^2.13.0 path-parse: ^1.0.7 supports-preserve-symlinks-flag: ^1.0.0 bin: resolve: bin/resolve - checksum: d13bf66d4e2ee30d291491f16f2fa44edd4e0cefb85d53249dd6f93e70b2b8c20ec62f01b18662e3cd40e50a7528f18c4087a99490048992a3bb954cf3201a5b + checksum: f8a26958aa572c9b064562750b52131a37c29d072478ea32e129063e2da7f83e31f7f11e7087a18225a8561cfe8d2f0df9dbea7c9d331a897571c0a2527dbb4c languageName: node linkType: hard "resolve@npm:^2.0.0-next.4": - version: 2.0.0-next.4 - resolution: "resolve@npm:2.0.0-next.4" + version: 2.0.0-next.5 + resolution: "resolve@npm:2.0.0-next.5" dependencies: - is-core-module: ^2.9.0 + is-core-module: ^2.13.0 path-parse: ^1.0.7 supports-preserve-symlinks-flag: ^1.0.0 bin: resolve: bin/resolve - checksum: c438ac9a650f2030fd074219d7f12ceb983b475da2d89ad3d6dd05fbf6b7a0a8cd37d4d10b43cb1f632bc19f22246ab7f36ebda54d84a29bfb2910a0680906d3 + checksum: a73ac69a1c4bd34c56b213d91f5b17ce390688fdb4a1a96ed3025cc7e08e7bfb90b3a06fcce461780cb0b589c958afcb0080ab802c71c01a7ecc8c64feafc89f languageName: node linkType: hard "resolve@patch:resolve@^1.1.6#~builtin": - version: 1.22.6 - resolution: "resolve@patch:resolve@npm%3A1.22.6#~builtin::version=1.22.6&hash=07638b" + version: 1.22.8 + resolution: "resolve@patch:resolve@npm%3A1.22.8#~builtin::version=1.22.8&hash=07638b" dependencies: is-core-module: ^2.13.0 path-parse: ^1.0.7 supports-preserve-symlinks-flag: ^1.0.0 bin: resolve: bin/resolve - checksum: 9d3b3c67aefd12cecbe5f10ca4d1f51ea190891096497c43f301b086883b426466918c3a64f1bbf1788fabb52b579d58809614006c5d0b49186702b3b8fb746a + checksum: 5479b7d431cacd5185f8db64bfcb7286ae5e31eb299f4c4f404ad8aa6098b77599563ac4257cb2c37a42f59dfc06a1bec2bcf283bb448f319e37f0feb9a09847 languageName: node linkType: hard "resolve@patch:resolve@^2.0.0-next.4#~builtin": - version: 2.0.0-next.4 - resolution: "resolve@patch:resolve@npm%3A2.0.0-next.4#~builtin::version=2.0.0-next.4&hash=07638b" + version: 2.0.0-next.5 + resolution: "resolve@patch:resolve@npm%3A2.0.0-next.5#~builtin::version=2.0.0-next.5&hash=07638b" dependencies: - is-core-module: ^2.9.0 + is-core-module: ^2.13.0 path-parse: ^1.0.7 supports-preserve-symlinks-flag: ^1.0.0 bin: resolve: bin/resolve - checksum: 4bf9f4f8a458607af90518ff73c67a4bc1a38b5a23fef2bb0ccbd45e8be89820a1639b637b0ba377eb2be9eedfb1739a84cde24fe4cd670c8207d8fea922b011 + checksum: 064d09c1808d0c51b3d90b5d27e198e6d0c5dad0eb57065fd40803d6a20553e5398b07f76739d69cbabc12547058bec6b32106ea66622375fb0d7e8fca6a846c languageName: node linkType: hard @@ -3958,22 +3950,22 @@ __metadata: languageName: node linkType: hard -"rollup@npm:^4.1.4": - version: 4.1.5 - resolution: "rollup@npm:4.1.5" - dependencies: - "@rollup/rollup-android-arm-eabi": 4.1.5 - "@rollup/rollup-android-arm64": 4.1.5 - "@rollup/rollup-darwin-arm64": 4.1.5 - "@rollup/rollup-darwin-x64": 4.1.5 - "@rollup/rollup-linux-arm-gnueabihf": 4.1.5 - "@rollup/rollup-linux-arm64-gnu": 4.1.5 - "@rollup/rollup-linux-arm64-musl": 4.1.5 - "@rollup/rollup-linux-x64-gnu": 4.1.5 - "@rollup/rollup-linux-x64-musl": 4.1.5 - "@rollup/rollup-win32-arm64-msvc": 4.1.5 - "@rollup/rollup-win32-ia32-msvc": 4.1.5 - "@rollup/rollup-win32-x64-msvc": 4.1.5 +"rollup@npm:^4.2.0": + version: 4.4.0 + resolution: "rollup@npm:4.4.0" + dependencies: + "@rollup/rollup-android-arm-eabi": 4.4.0 + "@rollup/rollup-android-arm64": 4.4.0 + "@rollup/rollup-darwin-arm64": 4.4.0 + "@rollup/rollup-darwin-x64": 4.4.0 + "@rollup/rollup-linux-arm-gnueabihf": 4.4.0 + "@rollup/rollup-linux-arm64-gnu": 4.4.0 + "@rollup/rollup-linux-arm64-musl": 4.4.0 + "@rollup/rollup-linux-x64-gnu": 4.4.0 + "@rollup/rollup-linux-x64-musl": 4.4.0 + "@rollup/rollup-win32-arm64-msvc": 4.4.0 + "@rollup/rollup-win32-ia32-msvc": 4.4.0 + "@rollup/rollup-win32-x64-msvc": 4.4.0 fsevents: ~2.3.2 dependenciesMeta: "@rollup/rollup-android-arm-eabi": @@ -4004,7 +3996,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: f323c5f61e49892e565c46f0114908ba58cc6139a2c1940181f3c82f8ba1bd608fabc833f35b679113b156dc1960ad5aa8bdf7ef987a9116f8db767293c52d95 + checksum: 90fdc55bfd247dc07c04901e1b802083516c0982e6b32d2fbccee6968fb944482207accdea2f5dc01198d36a5eb9356f9be451942c45c006a87a3bf50add1803 languageName: node linkType: hard @@ -4083,10 +4075,15 @@ __metadata: languageName: node linkType: hard -"set-blocking@npm:^2.0.0": - version: 2.0.0 - resolution: "set-blocking@npm:2.0.0" - checksum: 6e65a05f7cf7ebdf8b7c75b101e18c0b7e3dff4940d480efed8aad3a36a4005140b660fa1d804cb8bce911cac290441dc728084a30504d3516ac2ff7ad607b02 +"set-function-length@npm:^1.1.1": + version: 1.1.1 + resolution: "set-function-length@npm:1.1.1" + dependencies: + define-data-property: ^1.1.1 + get-intrinsic: ^1.2.1 + gopd: ^1.0.1 + has-property-descriptors: ^1.0.0 + checksum: c131d7569cd7e110cafdfbfbb0557249b538477624dfac4fc18c376d879672fa52563b74029ca01f8f4583a8acb35bb1e873d573a24edb80d978a7ee607c6e06 languageName: node linkType: hard @@ -4148,7 +4145,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": +"signal-exit@npm:^3.0.3": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 @@ -4176,18 +4173,18 @@ __metadata: languageName: node linkType: hard -"socks-proxy-agent@npm:^7.0.0": - version: 7.0.0 - resolution: "socks-proxy-agent@npm:7.0.0" +"socks-proxy-agent@npm:^8.0.1": + version: 8.0.2 + resolution: "socks-proxy-agent@npm:8.0.2" dependencies: - agent-base: ^6.0.2 - debug: ^4.3.3 - socks: ^2.6.2 - checksum: 720554370154cbc979e2e9ce6a6ec6ced205d02757d8f5d93fe95adae454fc187a5cbfc6b022afab850a5ce9b4c7d73e0f98e381879cf45f66317a4895953846 + agent-base: ^7.0.2 + debug: ^4.3.4 + socks: ^2.7.1 + checksum: 4fb165df08f1f380881dcd887b3cdfdc1aba3797c76c1e9f51d29048be6e494c5b06d68e7aea2e23df4572428f27a3ec22b3d7c75c570c5346507433899a4b6d languageName: node linkType: hard -"socks@npm:^2.6.2": +"socks@npm:^2.7.1": version: 2.7.1 resolution: "socks@npm:2.7.1" dependencies: @@ -4213,6 +4210,15 @@ __metadata: languageName: node linkType: hard +"split2@npm:^3.2.2": + version: 3.2.2 + resolution: "split2@npm:3.2.2" + dependencies: + readable-stream: ^3.0.0 + checksum: 8127ddbedd0faf31f232c0e9192fede469913aa8982aa380752e0463b2e31c2359ef6962eb2d24c125bac59eeec76873678d723b1c7ff696216a1cd071e3994a + languageName: node + linkType: hard + "ssri@npm:^10.0.0": version: 10.0.5 resolution: "ssri@npm:10.0.5" @@ -4236,7 +4242,7 @@ __metadata: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.3": +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -4431,6 +4437,15 @@ __metadata: languageName: node linkType: hard +"through2@npm:^4.0.2": + version: 4.0.2 + resolution: "through2@npm:4.0.2" + dependencies: + readable-stream: 3 + checksum: ac7430bd54ccb7920fd094b1c7ff3e1ad6edd94202e5528331253e5fde0cc56ceaa690e8df9895de2e073148c52dfbe6c4db74cacae812477a35660090960cc0 + languageName: node + linkType: hard + "tinybench@npm:^2.5.0": version: 2.5.1 resolution: "tinybench@npm:2.5.1" @@ -4479,6 +4494,13 @@ __metadata: languageName: node linkType: hard +"treeify@npm:^1.1.0": + version: 1.1.0 + resolution: "treeify@npm:1.1.0" + checksum: aa00dded220c1dd052573bd6fc2c52862f09870851a284f0d3650d72bf913ba9b4f6b824f4f1ab81899bae29375f4266b07fe47cbf82343a1efa13cc09ce87af + languageName: node + linkType: hard + "ts-interface-checker@npm:^0.1.9": version: 0.1.13 resolution: "ts-interface-checker@npm:0.1.13" @@ -4649,6 +4671,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: 3192ef6f3fd5df652f2dc1cd782b49d6ff14dc98e5dced492aa8a8c65425227da5da6aafe22523c67f035a272c599bb89cfe803c1db6311e44bed3042fc25487 + languageName: node + linkType: hard + "unique-filename@npm:^3.0.0": version: 3.0.0 resolution: "unique-filename@npm:3.0.0" @@ -4709,13 +4738,13 @@ __metadata: linkType: hard "vite@npm:^3.0.0 || ^4.0.0 || ^5.0.0-0, vite@npm:^3.1.0 || ^4.0.0 || ^5.0.0-0": - version: 5.0.0-beta.13 - resolution: "vite@npm:5.0.0-beta.13" + version: 5.0.0-beta.18 + resolution: "vite@npm:5.0.0-beta.18" dependencies: esbuild: ^0.19.3 fsevents: ~2.3.3 postcss: ^8.4.31 - rollup: ^4.1.4 + rollup: ^4.2.0 peerDependencies: "@types/node": ^18.0.0 || >=20.0.0 less: "*" @@ -4744,7 +4773,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 92b940f126a984659e5b7200b4728023cd8dfcee1d7b1b8b6da92129a8b3d4374b33db99b2e81cddaa4e98a9f2ea7287a7824b39053d2ed295681bbbdf913211 + checksum: 9481814791af3932266fa05112779d51d2f330b964d3472ca3373861b0a3c33169c55b5bc5fa82752f7d046ff320dfaaa0db77f2b78ec8cbe3dd6310eb7e8d69 languageName: node linkType: hard @@ -4871,20 +4900,20 @@ __metadata: languageName: node linkType: hard -"which-typed-array@npm:^1.1.11, which-typed-array@npm:^1.1.9": - version: 1.1.11 - resolution: "which-typed-array@npm:1.1.11" +"which-typed-array@npm:^1.1.11, which-typed-array@npm:^1.1.13, which-typed-array@npm:^1.1.9": + version: 1.1.13 + resolution: "which-typed-array@npm:1.1.13" dependencies: available-typed-arrays: ^1.0.5 - call-bind: ^1.0.2 + call-bind: ^1.0.4 for-each: ^0.3.3 gopd: ^1.0.1 has-tostringtag: ^1.0.0 - checksum: 711ffc8ef891ca6597b19539075ec3e08bb9b4c2ca1f78887e3c07a977ab91ac1421940505a197758fb5939aa9524976d0a5bbcac34d07ed6faa75cedbb17206 + checksum: 3828a0d5d72c800e369d447e54c7620742a4cc0c9baf1b5e8c17e9b6ff90d8d861a3a6dd4800f1953dbf80e5e5cec954a289e5b4a223e3bee4aeb1f8c5f33309 languageName: node linkType: hard -"which@npm:^2.0.1, which@npm:^2.0.2": +"which@npm:^2.0.1": version: 2.0.2 resolution: "which@npm:2.0.2" dependencies: @@ -4895,6 +4924,17 @@ __metadata: languageName: node linkType: hard +"which@npm:^4.0.0": + version: 4.0.0 + resolution: "which@npm:4.0.0" + dependencies: + isexe: ^3.1.1 + bin: + node-which: bin/which.js + checksum: f17e84c042592c21e23c8195108cff18c64050b9efb8459589116999ea9da6dd1509e6a1bac3aeebefd137be00fabbb61b5c2bc0aa0f8526f32b58ee2f545651 + languageName: node + linkType: hard + "why-is-node-running@npm:^2.2.2": version: 2.2.2 resolution: "why-is-node-running@npm:2.2.2" @@ -4907,16 +4947,7 @@ __metadata: languageName: node linkType: hard -"wide-align@npm:^1.1.5": - version: 1.1.5 - resolution: "wide-align@npm:1.1.5" - dependencies: - string-width: ^1.0.2 || 2 || 3 || 4 - checksum: d5fc37cd561f9daee3c80e03b92ed3e84d80dde3365a8767263d03dacfc8fa06b065ffe1df00d8c2a09f731482fcacae745abfbb478d4af36d0a891fad4834d3 - languageName: node - linkType: hard - -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" dependencies: @@ -4945,6 +4976,13 @@ __metadata: languageName: node linkType: hard +"y18n@npm:^5.0.5": + version: 5.0.8 + resolution: "y18n@npm:5.0.8" + checksum: 54f0fb95621ee60898a38c572c515659e51cc9d9f787fb109cef6fde4befbe1c4602dc999d30110feee37456ad0f1660fa2edcfde6a9a740f86a290999550d30 + languageName: node + linkType: hard + "yallist@npm:^4.0.0": version: 4.0.0 resolution: "yallist@npm:4.0.0" @@ -4959,6 +4997,28 @@ __metadata: languageName: node linkType: hard +"yargs-parser@npm:^20.2.2": + version: 20.2.9 + resolution: "yargs-parser@npm:20.2.9" + checksum: 8bb69015f2b0ff9e17b2c8e6bfe224ab463dd00ca211eece72a4cd8a906224d2703fb8a326d36fdd0e68701e201b2a60ed7cf81ce0fd9b3799f9fe7745977ae3 + languageName: node + linkType: hard + +"yargs@npm:^16.2.0": + version: 16.2.0 + resolution: "yargs@npm:16.2.0" + dependencies: + cliui: ^7.0.2 + escalade: ^3.1.1 + get-caller-file: ^2.0.5 + require-directory: ^2.1.1 + string-width: ^4.2.0 + y18n: ^5.0.5 + yargs-parser: ^20.2.2 + checksum: b14afbb51e3251a204d81937c86a7e9d4bdbf9a2bcee38226c900d00f522969ab675703bee2a6f99f8e20103f608382936034e64d921b74df82b63c07c5e8f59 + languageName: node + linkType: hard + "yocto-queue@npm:^0.1.0": version: 0.1.0 resolution: "yocto-queue@npm:0.1.0" From d2987bd3082a72f92754f26675fd0dac9c98c67c Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Mon, 13 Nov 2023 07:06:28 -0600 Subject: [PATCH 11/50] Create separate `NPM` command for `@typescript/analyze-trace` --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 62f7f7ddd..d53c4a820 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "test": "node --expose-gc ./node_modules/vitest/dist/cli-wrapper.js run", "test:cov": "vitest run --coverage", "type-check": "vitest --run typecheck", - "test:typescript": "tsc --noEmit -p typescript_test/tsconfig.json --generateTrace trace && npx @typescript/analyze-trace trace && rimraf trace" + "type-check:trace": "vitest --run typecheck && tsc --noEmit -p typescript_test/tsconfig.json --generateTrace trace && npx @typescript/analyze-trace trace && rimraf trace", + "test:typescript": "tsc --noEmit -p typescript_test/tsconfig.json" }, "keywords": [ "react", From 1e091afea64eb063693f11ec6b7fcdc90e22cb65 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Mon, 13 Nov 2023 07:12:00 -0600 Subject: [PATCH 12/50] Add JSDocs for `MergeParameters`. --- src/versionedTypes/ts47-mergeParameters.ts | 101 +++++++++++++++------ 1 file changed, 75 insertions(+), 26 deletions(-) diff --git a/src/versionedTypes/ts47-mergeParameters.ts b/src/versionedTypes/ts47-mergeParameters.ts index 5be0dfc43..10092b40a 100644 --- a/src/versionedTypes/ts47-mergeParameters.ts +++ b/src/versionedTypes/ts47-mergeParameters.ts @@ -4,65 +4,114 @@ import type { AnyFunction } from '@internal/types' /** + * Represents the longest array within an array of arrays. + * + * @template ArrayOfTuples An array of arrays. + * * @internal */ -type LongestTuple = T extends [ - infer U extends unknown[] -] - ? U - : T extends [infer U, ...infer R extends unknown[][]] - ? MostProperties> - : never +type LongestTuple = + ArrayOfTuples extends [infer FirstArray extends unknown[]] + ? FirstArray + : ArrayOfTuples extends [ + infer FirstArray, + ...infer RestArrays extends unknown[][] + ] + ? LongerOfTwo> + : never /** + * Determines the longer of two array types. + * + * @template ArrayOne First array type. + * @template ArrayTwo Second array type. + * * @internal */ -type MostProperties = keyof U extends keyof T ? T : U +type LongerOfTwo = keyof ArrayTwo extends keyof ArrayOne + ? ArrayOne + : ArrayTwo /** + * Extracts the element at a specific index in an array. + * + * @template ArrayType The array type. + * @template Index The index type. + * * @internal */ -type ElementAt = N extends keyof T - ? T[N] - : unknown +type ElementAt< + ArrayType extends unknown[], + Index extends PropertyKey +> = Index extends keyof ArrayType ? ArrayType[Index] : unknown /** + * Maps each array in an array of arrays to its element at a given index. + * + * @template ArrayOfTuples An array of arrays. + * @template Index The index to extract from each array. + * * @internal */ -type ElementsAt = { - [K in keyof T]: ElementAt +type ElementsAtGivenIndex< + ArrayOfTuples extends readonly unknown[][], + Index extends PropertyKey +> = { + [ArrayIndex in keyof ArrayOfTuples]: ElementAt< + ArrayOfTuples[ArrayIndex], + Index + > } /** + * Computes the intersection of all types in a tuple. + * + * @template Tuple A tuple of types. + * * @internal */ -type Intersect = T extends [] +type Intersect = Tuple extends [] ? unknown - : T extends [infer H, ...infer T] - ? H & Intersect - : T[number] + : Tuple extends [infer Head, ...infer Tail] + ? Head & Intersect + : Tuple[number] /** + * Merges a tuple of arrays into a single tuple, intersecting types at each index. + * + * @template ArrayOfTuples An array of tuples. + * @template LongestArray The longest array in ArrayOfTuples. + * * @internal */ type MergeTuples< - T extends readonly unknown[][], - L extends unknown[] = LongestTuple + ArrayOfTuples extends readonly unknown[][], + LongestArray extends unknown[] = LongestTuple > = { - [K in keyof L]: Intersect> + [Index in keyof LongestArray]: Intersect< + ElementsAtGivenIndex + > } /** + * Extracts the parameter types from a tuple of functions. + * + * @template FunctionsArray An array of function types. + * * @internal */ -type ExtractParameters = { - [K in keyof T]: Parameters +type ExtractParameters = { + [Index in keyof FunctionsArray]: Parameters } /** + * Merges the parameters of a tuple of functions into a single tuple. + * + * @template FunctionsArray An array of function types. + * * @internal */ -export type MergeParameters = - '0' extends keyof T - ? MergeTuples> - : Parameters +export type MergeParameters = + '0' extends keyof FunctionsArray + ? MergeTuples> + : Parameters From 5ff1968b8d2fa4fce8423294715b7533284ffd28 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Mon, 13 Nov 2023 07:14:46 -0600 Subject: [PATCH 13/50] Fix TS bug related to `OutputSelector`. - Add `IfUnknown` utility type. - Add `FallbackIfUnknown` utility type. - Fix TS issue related to `OutputSelector` which was stemming from `ExtractReturnType`. --- src/types.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index 276070c5f..9c236930f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -517,7 +517,7 @@ export type FunctionType = Extract */ export type ExtractReturnType = { [Index in keyof FunctionsArray]: FunctionsArray[Index] extends FunctionsArray[number] - ? ReturnType + ? FallbackIfUnknown, any> : never } @@ -712,6 +712,28 @@ export type SetRequired = Omit< > & Required> +/** + * An if-else-like type that resolves depending on whether the given type is `unknown`. + * @see {@link https://github.com/sindresorhus/type-fest/blob/main/source/if-unknown.d.ts Source} + * + * @internal + */ +export type IfUnknown = unknown extends T // `T` can be `unknown` or `any` + ? [T] extends [null] // `any` can be `null`, but `unknown` can't be + ? TypeIfNotUnknown + : TypeIfUnknown + : TypeIfNotUnknown + +/** + * When a type is resolves to `unknown`, fallback to a different type. + * + * @template T - Type to be checked. + * @template FallbackTo - Type to fallback to if `T` resolves to `unknown`. + * + * @internal + */ +export type FallbackIfUnknown = IfUnknown + /** * * ----------------------------------------------------------------------------- From 72010838175834e61cceed8410924d2fa094f56f Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Mon, 13 Nov 2023 08:06:05 -0600 Subject: [PATCH 14/50] Add JSDocs for `createCurriedSelector` - Add JSDocs for: * `createCurriedSelector` * `createCurriedSelectorCreator` * `CreateCurriedSelector` * `Curried` utility type. * `CurriedOutputSelector` --- src/createCurriedSelectorCreator.ts | 166 ++++++++++++++++++++++++++-- src/index.ts | 1 + src/types.ts | 58 ++++++---- 3 files changed, 196 insertions(+), 29 deletions(-) diff --git a/src/createCurriedSelectorCreator.ts b/src/createCurriedSelectorCreator.ts index 7f09f22ca..87be70841 100644 --- a/src/createCurriedSelectorCreator.ts +++ b/src/createCurriedSelectorCreator.ts @@ -14,14 +14,33 @@ import type { } from './types' /** - * @WIP + * An instance of `createCurriedSelector`, customized with a given memoize implementation. + * + * @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createCurriedSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). + * @template ArgsMemoizeFunction - The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createCurriedSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used. + * + * @since 5.0.0 + * @public + * @experimental */ export interface CreateCurriedSelector< MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize > { /** - * One arg + * Creates a memoized "curried" selector function. + * + * @param createSelectorArgs - An arbitrary number of input selectors as separate inline arguments and a `combiner` function. + * @returns A memoized "curried" output selector. + * + * @template InputSelectors - The type of the input selectors as an array. + * @template Result - The return type of the `combiner` as well as the output selector. + * @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`. + * @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`. + * + * @since 5.0.0 + * @public + * @experimental */ ( ...createSelectorArgs: [ @@ -37,7 +56,19 @@ export interface CreateCurriedSelector< InterruptRecursion /** - * inline args + * Creates a memoized "curried" selector function. + * + * @param createSelectorArgs - An arbitrary number of input selectors as separate inline arguments, a `combiner` function and an `options` object. + * @returns A memoized "curried" output selector. + * + * @template InputSelectors - The type of the input selectors as an array. + * @template Result - The return type of the `combiner` as well as the output selector. + * @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`. + * @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`. + * + * @since 5.0.0 + * @public + * @experimental */ < InputSelectors extends SelectorArray, @@ -66,7 +97,21 @@ export interface CreateCurriedSelector< InterruptRecursion /** - * array args + * Creates a memoized "curried" selector function. + * + * @param inputSelectors - An array of input selectors. + * @param combiner - A function that Combines the input selectors and returns an output selector. Otherwise known as the result function. + * @param createSelectorOptions - An optional options object that allows for further customization per selector. + * @returns A memoized "curried" output selector. + * + * @template InputSelectors - The type of the input selectors array. + * @template Result - The return type of the `combiner` as well as the output selector. + * @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`. + * @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`. + * + * @since 5.0.0 + * @public + * @experimental */ < InputSelectors extends SelectorArray, @@ -92,9 +137,65 @@ export interface CreateCurriedSelector< > & InterruptRecursion } - /** - * @WIP + * Creates a "curried" selector creator function with the specified memoization function and options for customizing memoization behavior. + * + * @param createSelectorCreatorArgs - Same parameters as {@linkcode createSelectorCreator createSelectorCreator} + * @returns A customized `createCurriedSelector` function. + * + * @example + * ```ts + * import { createCurriedSelectorCreator, createSelector, weakMapMemoize } from 'reselect' + * import { useSelector } from 'react-redux' + * + * interface RootState { + * todos: { id: number; completed: boolean }[] + * alerts: { id: number; read: boolean }[] + * } + * + * const state: RootState = { + * todos: [ + * { id: 0, completed: false }, + * { id: 1, completed: true } + * ], + * alerts: [ + * { id: 0, read: false }, + * { id: 1, read: true } + * ] + * } + * + * const parametricSelector = createSelector( + * [(state: RootState) => state.todos, (state: RootState, id: number) => id], + * (todos, id) => todos.filter(todo => todo.id === id), + * { memoize: weakMapMemoize, argsMemoize: weakMapMemoize } + * ) + * + * const createCurriedSelectorWeakMap = createCurriedSelectorCreator({ memoize: weakMapMemoize, argsMemoize: weakMapMemoize }) + * + * const curriedSelector = createCurriedSelectorWeakMap( + * [(state: RootState) => state.todos, (state: RootState, id: number) => id], + * (todos, id) => todos.filter(todo => todo.id === id) + * ) + * + * // This: + * parametricSelector(state, 0) + * + * // Is the same as this: + * curriedSelector(0)(state) + * + * // Inside your component you can replace this: + * const selectTodo = useSelector(state => parametricSelector(state, id)) + * + * // With this: + * const selectTodo = useSelector(curriedSelector(id)) + * ``` + * + * @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createCurriedSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). + * @template ArgsMemoizeFunction - The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createCurriedSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used. + * + * @since 5.0.0 + * @public + * @experimental */ export function createCurriedSelectorCreator< MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, @@ -109,7 +210,6 @@ export function createCurriedSelectorCreator< ) => { // @ts-ignore const selector = createSelector.apply(null, createSelectorArgs) - // const selector = createSelector(...createSelectorArgs) const curriedSelector = selector.argsMemoize( (...params: DropFirstParameter) => { return selector.argsMemoize((state: Parameters[0]) => { @@ -126,7 +226,57 @@ export function createCurriedSelectorCreator< } /** - * @WIP + * Accepts one or more "input selectors" (either as separate arguments or a single array), + * a single "result function" / "combiner", and an optional options object, and + * generates a memoized "curried" selector function. + * + * @example + * ```ts + * import { createCurriedSelector, createSelector } from 'reselect' + * import { useSelector } from 'react-redux' + * + * interface RootState { + * todos: { id: number; completed: boolean }[] + * alerts: { id: number; read: boolean }[] + * } + * + * const state: RootState = { + * todos: [ + * { id: 0, completed: false }, + * { id: 1, completed: true } + * ], + * alerts: [ + * { id: 0, read: false }, + * { id: 1, read: true } + * ] + * } + * + * const parametricSelector = createSelector( + * [(state: RootState) => state.todos, (state: RootState, id: number) => id], + * (todos, id) => todos.filter(todo => todo.id === id) + * ) + * + * // This: + * parametricSelector(state, 0) + * + * const curriedSelector = createCurriedSelector( + * [(state: RootState) => state.todos, (state: RootState, id: number) => id], + * (todos, id) => todos.filter(todo => todo.id === id) + * ) + * + * // Is the same as this: + * curriedSelector(0)(state) + * + * // Inside your component you can replace this: + * const selectTodo = useSelector(state => parametricSelector(state, id)) + * + * // With this: + * const selectTodo = useSelector(curriedSelector(id)) + * ``` + * + * @since 5.0.0 + * @public + * @experimental */ export const createCurriedSelector = /* #__PURE__ */ createCurriedSelectorCreator(defaultMemoize) diff --git a/src/index.ts b/src/index.ts index ab800d4f9..a61ada271 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ export type { DefaultMemoizeOptions } from './defaultMemoize' export type { Combiner, CreateSelectorOptions, + CurriedOutputSelector, DefaultMemoizeFields, EqualityFn, ExtractMemoizerFields, diff --git a/src/types.ts b/src/types.ts index 9c236930f..5ac812ca7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -280,27 +280,6 @@ export type OutputSelector< ArgsMemoizeFunction > - -export type Curried any> = ( - ...params: DropFirstParameter -) => (state: Parameters[0]) => ReturnType - -export type CurriedOutputSelector< - InputSelectors extends SelectorArray = SelectorArray, - Result = unknown, - MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, - ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize -> = Curried< - OutputSelector -> & - ExtractMemoizerFields & - OutputSelectorFields< - InputSelectors, - Result, - MemoizeFunction, - ArgsMemoizeFunction - > - /** * A function that takes input selectors' return values as arguments and returns a result. Otherwise known as `resultFunc`. * @@ -456,6 +435,32 @@ export type DefaultMemoizeFields = { clearCache: () => void } +/** + * Represents a curried {@linkcode OutputSelector OutputSelector}. + * + * @template InputSelectors - The type of the input selectors. + * @template Result - The type of the result returned by the `resultFunc`. + * @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createCurriedSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). + * @template ArgsMemoizeFunction - The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createCurriedSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used. + * + * @public + */ +export type CurriedOutputSelector< + InputSelectors extends SelectorArray = SelectorArray, + Result = unknown, + MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize +> = Curried< + OutputSelector +> & + ExtractMemoizerFields & + OutputSelectorFields< + InputSelectors, + Result, + MemoizeFunction, + ArgsMemoizeFunction + > + /* * ----------------------------------------------------------------------------- * ----------------------------------------------------------------------------- @@ -589,6 +594,17 @@ export type AnyNonNullishValue = NonNullable */ export type InterruptRecursion = AnyNonNullishValue +/** + * Create a curried version of a function. + * + * @template FunctionType The original function type. + * + * @internal + */ +export type Curried any> = ( + ...params: DropFirstParameter +) => (state: Parameters[0]) => ReturnType + /* * ----------------------------------------------------------------------------- * ----------------------------------------------------------------------------- From 12e24110653cbf56abcbe88c03659a37b9887ad3 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Mon, 13 Nov 2023 09:49:28 -0600 Subject: [PATCH 15/50] Fix toggle sections in `README` - Add more examples to docs. - Fix badges in docs. - Fix Table of Contents. - Fix external references. - Fix internal links. - Make Terminology section toggled. - Remove `skortchmark9` chrome extension reference since it is no longer available. --- README.md | 947 +++++++++++++++++++++++++----------------------------- 1 file changed, 446 insertions(+), 501 deletions(-) diff --git a/README.md b/README.md index 04283c3c3..52e8860d0 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,20 @@ # Reselect -![TypeScript][typescript-badge][![npm package][npm-badge]][npm][![Coveralls][coveralls-badge]][coveralls][![GitHub Workflow Status][build-badge]][build] +[![npm package][npm-badge]][npm][![Coveralls][coveralls-badge]][coveralls][![GitHub Workflow Status][build-badge]][build]![TypeScript][typescript-badge] A library for creating memoized "selector" functions. Commonly used with Redux, but usable with any plain JS immutable data as well. -- Selectors can compute derived data, allowing Redux to store the minimal possible state. +- Selectors can compute derived data, allowing `Redux` to store the minimal possible state. - Selectors are efficient. A selector is not recomputed unless one of its arguments changes. - Selectors are composable. They can be used as input to other selectors. -The **Redux docs usage page on [Deriving Data with Selectors](https://redux.js.org/usage/deriving-data-selectors)** covers the purpose and motivation for selectors, why memoized selectors are useful, typical `Reselect` usage patterns, and using selectors with React-Redux. +The **Redux docs usage page on [Deriving Data with Selectors](https://redux.js.org/usage/deriving-data-selectors)** covers the purpose and motivation for selectors, why memoized selectors are useful, typical `Reselect` usage patterns, and using selectors with `React-Redux`. ## Installation ### Redux Toolkit -While `Reselect` is not exclusive to Redux, it is already included by default in [the official Redux Toolkit package](https://redux-toolkit.js.org) - no further installation needed. +While `Reselect` is not exclusive to `Redux`, it is already included by default in [the official Redux Toolkit package](https://redux-toolkit.js.org) - no further installation needed. ```ts import { createSelector } from '@reduxjs/toolkit' @@ -52,7 +52,7 @@ pnpm add reselect ## Basic Usage -`Reselect` exports a `createSelector` API, which generates memoized selector functions. `createSelector` accepts one or more `input selectors`, which extract values from arguments, and a `combiner` function that receives the extracted values and should return a derived value. If the generated `output selector` is called multiple times, the output will only be recalculated when the extracted values have changed. +`Reselect` exports a [`createSelector`] API, which generates memoized selector functions. [`createSelector`] accepts one or more [`input selectors`], which extract values from arguments, and a [`combiner`] function that receives the extracted values and should return a derived value. If the generated [`output selector`] is called multiple times, the output will only be recalculated when the extracted values have changed. You can play around with the following **example** in [this CodeSandbox](https://codesandbox.io/s/reselect-example-g3k9gf?file=/src/index.js): @@ -115,19 +115,26 @@ Another difference is that with `memoizedSelectCompletedTodos` the referential i - [Redux Toolkit](#redux-toolkit) - [Standalone](#standalone) - [Basic Usage](#basic-usage) +- [Terminology](#terminology) +- [How Does `Reselect` Work?](#how-does-reselect-work) + - [Cascading Memoization](#cascading-memoization) + - [`Reselect` Vs Standard Memoization](#reselect-vs-standard-memoization) + - [Why `Reselect` Is Often Used With `Redux`](#why-reselect-is-often-used-with-redux) - [API](#api) - - [**`createSelector`**](#createselector) - - [**`defaultMemoize`**](#defaultmemoize) - - [**`weakMapMemoize`**](#weakmapmemoize) - - [**`autotrackMemoize`**](#autotrackmemoize) - - [**`createSelectorCreator`**](#createselectorcreator) - - [**`createStructuredSelector`**](#createstructuredselector) - - [**`createCurriedSelector`**](#createcurriedselector) + - [**`createSelector`**][`createSelector`] + - [**`defaultMemoize`**][`defaultMemoize`] + - [**`weakMapMemoize`**][`weakMapMemoize`] + - [**`autotrackMemoize`**][`autotrackMemoize`] + - [**`createSelectorCreator`**][`createSelectorCreator`] + - [**`createStructuredSelector`**][`createStructuredSelector`] + - [**`createCurriedSelector`**][`createCurriedSelector`] - [Debugging Tools](#debuggingtools) +- [What's New in 5.0.0?](#v5summary) +- [Optimizing `Reselect`](#optimizing-reselect) - [FAQ](#faq) - [Why isn’t my selector recomputing when the input state changes?](#why-isnt-my-selector-recomputing-when-the-input-state-changes) - [Why is my selector recomputing when the input state stays the same?](#why-is-my-selector-recomputing-when-the-input-state-stays-the-same) - - [Can I use Reselect without Redux?](#can-i-use-reselect-without-redux) + - [Can I use `Reselect` without `Redux`?](#can-i-use-reselect-without-redux) - [How do I create a selector that takes an argument?](#how-do-i-create-a-selector-that-takes-an-argument) - [The default memoization function is no good, can I use a different one?](#the-default-memoization-function-is-no-good-can-i-use-a-different-one) - [How do I test a selector?](#how-do-i-test-a-selector) @@ -135,28 +142,27 @@ Another difference is that with `memoizedSelectCompletedTodos` the referential i - [Are there TypeScript Typings?](#are-there-typescript-typings) - [I am seeing a TypeScript error: `Type instantiation is excessively deep and possibly infinite`](#i-am-seeing-a-typescript-error-type-instantiation-is-excessively-deep-and-possibly-infinite) - [How can I make a curried selector?](#how-can-i-make-a-curried-selector) + - [How can I make pre-typed version of `createSelector` for my root state?](#how-can-i-make-pre-typed-version-of-createselector-for-my-root-state) + - [What if I want to use `createSelector` without memoization?](#what-if-i-want-to-use-createselector-without-memoization) +- [External References](#external-references) - [Related Projects](#related-projects) - [License](#license) - [Prior Art and Inspiration](#prior-art-and-inspiration) --- -
- - - ## Terminology - +
Click to expand -- [**`Selector Function`**](#selector-function): Any function that accepts the Redux store state (or part of the state) as an argument, and returns data that is based on that state. -- [**`Input Selectors`**](#input-selectors): Standard selector functions that are used to create the result function. -- [**`Output Selector`**](#output-selector): The actual selectors generated by calling `createSelector`. -- [**`Result Function`**](#result-function): The function that comes after the input selectors. It takes input selectors' return values as arguments and returns a result. Otherwise known as the `combiner`. -- [**`Combiner`**](#combiner): A function that takes input selectors' return values as arguments and returns a result. This term is somewhat interchangeably used with `resultFunc`. But `combiner` is more of a general term and `resultFunc` is more specific to `Reselect`. So the `resultFunc` is a `combiner` but a `combiner` is not necessarily the same as `resultFunc`. -- [**`Dependencies`**](#dependencies): Same as input selectors. They are what the output selector "depends" on. +- [**`Selector Function`**](#selector-function): A function that takes the `Redux` store state (or a part of it) as an argument and returns data derived from that state. +- [**`input selectors`**](#input-selectors): Basic selector functions used as building blocks for creating a memoized selector. They are passed as the first argument(s) to [`createSelector`]. +- [**`Output Selector`**](#output-selector): The actual memoized selectors created by [`createSelector`]. +- [**`Result Function (Combiner)`**](#result-function): The function that comes after the [`input selectors`]. It takes the [`input selectors`]' return values as arguments and returns a result. Otherwise known as the `combiner`. +- [**`Combiner`**](#combiner): A function that takes [`input selectors`]' return values as arguments and returns a result (This function comes after the [`input selectors`] inside [`createSelector`]). This term is somewhat interchangeably used with `Result Function` or `resultFunc`. But `combiner` is more of a general term and `resultFunc` is more specific to `Reselect`. So the `resultFunc` is a `combiner` but a `combiner` is not necessarily the same as `resultFunc`. For the sake of simplicity, they are used synonymously throughout this documentation. +- [**`Dependencies`**](#dependencies): Same as [`input selectors`]. They are what the [`output selector`] "depends" on. -The below example serves as a visual aid. +The below example serves as a visual aid: ```ts const outputSelector = createSelector( @@ -177,46 +183,34 @@ const outputSelector = createSelector( ### Cascading Memoization - +
Click to expand The way `Reselect` works can be broken down into multiple parts: -1. **Initial Run**: On the first call, `Reselect` runs all the `input selectors`, gathers their results, and passes them to the `result function`. +1. **Initial Run**: On the first call, `Reselect` runs all the [`input selectors`], gathers their results, and passes them to the [`result function`]. 2. **Subsequent Runs**: For subsequent calls, `Reselect` performs two levels of checks: - - **First Level**: It compares the current arguments with the previous ones. + - **First Level**: It compares the current arguments with the previous ones (done by `argsMemoize`). - - If they're the same, it returns the cached result without running the `input selectors` or the `result function`. + - If they're the same, it returns the cached result without running the [`input selectors`] or the [`result function`]. - If they differ, it proceeds to the second level. - - **Second Level**: It runs the `input selectors` and compares their current results with the previous ones. + - **Second Level**: It runs the [`input selectors`] and compares their current results with the previous ones (done by `memoize`). > [!NOTE] - > If any one of the `input selectors` return a different result, all `input selectors` will recalculate. - - If the results are the same, it returns the cached result without running the `result function`. - - If the results differ, it runs the `result function`. + > If any one of the [`input selectors`] return a different result, all [`input selectors`] will recalculate. + - If the results are the same, it returns the cached result without running the [`result function`]. + - If the results differ, it runs the [`result function`]. This behavior is what we call **_Cascading Double-Layer Memoization_**. - +
### `Reselect` Vs Standard Memoization +
Click to expand + ##### Standard Memoization ![normal-memoization-function](docs/assets//normal-memoization-function.png) @@ -227,16 +221,16 @@ _Standard memoization only compares arguments. If they're the same, it returns t ![reselect-memoization](docs/assets//reselect-memoization.png) -_`Reselect` adds a second layer of checks with the `input selectors`. This is crucial in `Redux` applications where state references change frequently._ +_`Reselect` adds a second layer of checks with the [`input selectors`]. This is crucial in `Redux` applications where state references change frequently._ -A normal memoization function will compare the arguments, and if they are the same as last time, it will skip running the function and return the cached result. `Reselect`'s behavior differs from a simple memoization function in that, it adds a second layer of checks through the `input selectors`. So what can sometimes happen is that the arguments that get passed to the `input selectors` change, but the result of the `input selectors` still remain the same, and that means we can still skip running the `result function`. +A normal [memoization] function will compare the arguments, and if they are the same as last time, it will skip running the function and return the cached result. However, `Reselect` enhances this by introducing a second tier of checks via its [`input selectors`]. It's possible that the arguments passed to these [`input selectors`] may change, yet their results remain the same. When this occurs, `Reselect` avoids re-executing the [`result function`], and returns the cached result. -This is especially important since in a `Redux` application, your `state` is going to change its reference any time you make an update through dispatched actions. +This feature becomes crucial in `Redux` applications, where the `state` changes its reference anytime an `action` is dispatched. > [!NOTE] -> The `input selectors` take the same arguments as the `output selector`. +> The [`input selectors`] take the same arguments as the [`output selector`]. -#### Why `Reselect` Is Often Used With `Redux` +### Why `Reselect` Is Often Used With `Redux` Imagine you have a selector like this: @@ -288,7 +282,9 @@ store.dispatch(toggleRead(0)) selectCompletedTodos(store.getState()) // The `input selectors` will run, But the `result function` is skipped and the cached result will be returned. ``` -Even though our `state` changes, and we fail the comparison check on the arguments, the `result function` will not run because the current `state.todos` is still the same as the previous `state.todos`. So we have at least partial memoization, because the memoization "cascades" and goes from the arguments to the results of the `input selectors`. So basically there are "2 layers of checks" and in this situation, while the first one fails, the second one succeeds. And that is why this type of `Cascading Double-Layer Memoization` makes `Reselect` a good candidate to be used with `Redux`. +Even when the overall `state` changes, `Reselect` ensures efficient memoization through its unique approach. The [`result function`] doesn't re-run if the relevant part of the `state` (in this case `state.todos`), remains unchanged. This is due to `Reselect`'s [**_Cascading Double-Layer Memoization_**][**_Cascading Memoization_**]. The first layer checks the entire `state`, and the second layer checks the results of the [`input selectors`]. If the first layer fails (due to a change in the overall `state`) but the second layer succeeds (because `state.todos` is unchanged), `Reselect` skips recalculating the [`result function`]. This dual-check mechanism makes `Reselect` particularly effective in `Redux` applications, ensuring computations are only done when truly necessary. + +
@@ -296,66 +292,42 @@ Even though our `state` changes, and we fail the comparison check on the argumen ## API - + ### createSelector(...inputSelectors | [inputSelectors], resultFunc, createSelectorOptions?) -
- - - -#### Description +
Description -
- -Accepts one or more ["input selectors"](#input-selectors) (either as separate arguments or a single array), -a single ["result function"](#result-function) / ["combiner"](#combiner), and an optional options object, and +Accepts one or more ["input selectors"][`input selectors`] (either as separate arguments or a single array), +a single ["result function"][`result function`] / ["combiner"][`combiner`], and an optional options object, and generates a memoized selector function.
-
- - - -#### Parameters - - +
Parameters -| Name | Description | -| :----------------------- | :---------------------------------------------------------------------------------------------------------------------------- | -| `inputSelectors` | An array of [`input selectors`](#input-selectors), can also be passed as separate arguments. | -| `combiner` | A [`combiner`](#combiner) function that takes the results of the [`input selectors`](#input-selectors) as separate arguments. | -| `createSelectorOptions?` | An optional options object that allows for further customization per selector. | +| Name | Description | +| :----------------------- | :----------------------------------------------------------------------------------------------- | +| `inputSelectors` | An array of [`input selectors`], can also be passed as separate arguments. | +| `combiner` | A [`combiner`] function that takes the results of the [`input selectors`] as separate arguments. | +| `createSelectorOptions?` | An optional options object that allows for further customization per selector. |
-
- - +
Returns -#### Returns - -
- -A memoized [`output selector`](#output-selector). +A memoized [`output selector`].
-
- - - -#### Type parameters - - +
Type parameters -| Name | Description | -| :---------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `InputSelectors` | The type of the [`input selectors`](#input-selectors) array. | -| `Result` | The return type of the [`combiner`](#combiner) as well as the [`output selector`](#output-selector). | -| `OverrideMemoizeFunction` | The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`. | -| `OverrideArgsMemoizeFunction` | The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`. | +| Name | Description | +| :---------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `InputSelectors` | The type of the [`input selectors`] array. | +| `Result` | The return type of the [`combiner`] as well as the [`output selector`]. | +| `OverrideMemoizeFunction` | The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into [`createSelectorCreator`]. | +| `OverrideArgsMemoizeFunction` | The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into [`createSelectorCreator`]. |
@@ -369,19 +341,13 @@ A memoized [`output selector`](#output-selector). #### defaultMemoize(func, equalityCheckOrOptions = defaultEqualityCheck) -
- - - -##### Description +
Description -
- -The standard memoize function used by `createSelector`. +The standard memoize function used by [`createSelector`]. It has a default cache size of 1. This means it always recalculates when the value of an argument changes. However, this can be customized as needed with a specific max cache size (**`Since`** 4.1.0). -It determines if an argument has changed by calling the `equalityCheck` function. As `defaultMemoize` is designed to be used with immutable data, the default `equalityCheck` function checks for changes using [`reference equality`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality): +It determines if an argument has changed by calling the `equalityCheck` function. As `defaultMemoize` is designed to be used with immutable data, the default `equalityCheck` function checks for changes using [`reference equality`][Reference Equality Check]: ```ts const defaultEqualityCheck = (previousValue: any, currentValue: any) => { @@ -391,13 +357,7 @@ const defaultEqualityCheck = (previousValue: any, currentValue: any) => {
-
- - - -##### Parameters - - +
Parameters | Name | Description | | :----------------------- | :---------------------------------------------------------- | @@ -427,25 +387,13 @@ interface DefaultMemoizeOptions {
-
- - - -##### Returns - - +
Returns A memoized function with a `.clearCache()` method attached.
-
- - - -##### Type parameters - - +
Type parameters | Name | Description | | :----- | :----------------------------------------- | @@ -453,15 +401,9 @@ A memoized function with a `.clearCache()` method attached.
-
- - +
Examples -##### **`Examples`** - -
- -###### Using `createSelector` +###### Using [`createSelector`] ```ts import { shallowEqual } from 'react-redux' @@ -485,7 +427,7 @@ const selectTodoIds = createSelector( ) ``` -###### Using `createSelectorCreator` +###### Using [`createSelectorCreator`] ```ts import { shallowEqual } from 'react-redux' @@ -522,27 +464,25 @@ const selectTodoIds = createSelectorShallowEqual( #### weakMapMemoize(func) - (**`Since`** 5.0.0) -
+
Description - +[`defaultMemoize`] has to be explicitly configured to have a cache size larger than 1, and uses an LRU cache internally. -##### Description +`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 selectors). **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. - - -`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 selectors). **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. +
-**Design Tradeoffs for `weakMapMemoize`:** +
Design Tradeoffs - Pros: - 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 `WeakMap`s. + how long values are kept in cache as it's based on garbage collection and [`WeakMap`]s. - Cons: - There's currently no way to alter the argument comparisons. They're based on strict reference equality. -**Use Cases for `weakMapMemoize`:** +
+ +
Use Cases - 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 @@ -555,13 +495,7 @@ useSelector(state => selectSomeData(state, id))
-
- - - -##### Parameters - - +
Parameters | Name | Description | | :----- | :--------------------------- | @@ -569,25 +503,13 @@ useSelector(state => selectSomeData(state, id))
-
- - - -##### Returns - - +
Returns A memoized function with a `.clearCache()` method attached.
-
- - - -##### Type parameters - - +
Type parameters | Name | Description | | :----- | :----------------------------------------- | @@ -595,13 +517,7 @@ A memoized function with a `.clearCache()` method attached.
-
- - - -##### **`Examples`** - - +
Examples Prior to `weakMapMemoize`, you had this problem: @@ -619,7 +535,7 @@ parametricSelector(state, 0) // Selector runs again! Before you could solve this in a number of different ways: -1. Set the `maxSize` with `defaultMemoize`: +1. Set the `maxSize` with [`defaultMemoize`]: ```ts const parametricSelector = createSelector( @@ -635,7 +551,7 @@ const parametricSelector = createSelector( But this required having to know the cache size ahead of time. -2. Create unique selector instances using `useMemo`. +2. Create unique selector instances using [`useMemo`]. ```tsx const parametricSelector = (id: number) => @@ -658,7 +574,7 @@ const MyComponent: FC = ({ id }) => { } ``` -3. Using `useCallback`. +3. Using [`useCallback`]. ```tsx const parametricSelector = createSelector( @@ -681,7 +597,7 @@ const MyComponent: FC = ({ id }) => { } ``` -4. Use `re-reselect`: +4. Use [`re-reselect`]: ```ts import { createCachedSelector } from 're-reselect' @@ -694,7 +610,7 @@ const parametricSelector = createCachedSelector( Starting in 5.0.0, you can eliminate this problem using `weakMapMemoize`. -###### Using `createSelector` +###### Using [`createSelector`] ```ts const parametricSelector = createSelector( @@ -712,7 +628,7 @@ parametricSelector(state, 1) // Selector runs parametricSelector(state, 0) ``` -###### Using `createSelectorCreator` +###### Using [`createSelectorCreator`] ```ts import { createSelectorCreator, weakMapMemoize } from 'reselect' @@ -745,51 +661,53 @@ This solves the problem of having to know and set the cache size prior to creati #### autotrackMemoize(func) - (**`Since`** 5.0.0) -
- - - -##### Description - - +
Description 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.
+Design Tradeoffs - +- Pros: + - It is likely to avoid excess calculations and recalculate fewer times than defaultMemoize will, which may also result in fewer component re-renders. +- Cons: -##### Parameters + - 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: - + ```ts + createSelector([state => state.todos], todos => todos) + ``` -| Name | Description | -| :----- | :--------------------------- | -| `func` | The function to be memoized. | + that just immediately returns the extracted value will never update, because it doesn't see any field accesses to check.
+Use Cases - +- It 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. -##### Returns +
- +
Parameters -A memoized function with a `.clearCache()` method attached. +| Name | Description | +| :----- | :--------------------------- | +| `func` | The function to be memoized. |
-
+
Returns - +A memoized function with a `.clearCache()` method attached. -##### Type parameters +
- +
Type parameters | Name | Description | | :----- | :----------------------------------------- | @@ -797,15 +715,9 @@ A memoized function with a `.clearCache()` method attached.
-
- - - -##### **`Examples`** - - +
Examples -###### Using `createSelector` +###### Using [`createSelector`] ```ts import { @@ -820,7 +732,7 @@ const selectTodoIds = createSelector( ) ``` -###### Using `createSelectorCreator` +###### Using [`createSelectorCreator`] ```ts import { @@ -838,8 +750,6 @@ const selectTodoIds = createSelectorAutotrack( ) ``` -**Design Tradeoffs for autotrackMemoize:** - - Pros: - It is likely to avoid excess calculations and recalculate fewer times than defaultMemoize will, which may also result in fewer component re-renders. - Cons: @@ -858,40 +768,7 @@ const selectTodoIds = createSelectorAutotrack( - It 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. -Using `createSelector` - -```ts -import { - unstable_autotrackMemoize as autotrackMemoize, - createSelector -} from 'reselect' - -const selectTodoIds = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(todo => todo.id), - { memoize: autotrackMemoize } -) -``` - -Using `createSelectorCreator` - -```ts -import { - unstable_autotrackMemoize as autotrackMemoize, - createSelectorCreator -} from 'reselect' - -const createSelectorAutotrack = createSelectorCreator({ - memoize: autotrackMemoize -}) - -const selectTodoIds = createSelectorAutotrack( - [(state: RootState) => state.todos], - todos => todos.map(todo => todo.id) -) -``` - -
+
--> @@ -901,86 +778,50 @@ const selectTodoIds = createSelectorAutotrack( ### createSelectorCreator(memoize | options, ...memoizeOptions) -
- - - -#### Description - - +
Description Accepts either a `memoize` function and `...memoizeOptions` rest parameter, or **`Since`** 5.0.0 an `options` object containing a `memoize` function and creates a custom selector creator function.
-
- - - -#### Parameters (**`Since`** 5.0.0) - - +
Parameters (Since 5.0.0) | Name | Description | | :----------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `options` | An options object containing the `memoize` function responsible for memoizing the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). It also provides additional options for customizing memoization. While the `memoize` property is mandatory, the rest are optional. | -| `options.argsMemoize?` | The optional memoize function that is used to memoize the arguments passed into the `output selector` generated by `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`).
**`Default`** `defaultMemoize` | +| `options` | An options object containing the `memoize` function responsible for memoizing the `resultFunc` inside [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). It also provides additional options for customizing memoization. While the `memoize` property is mandatory, the rest are optional. | +| `options.argsMemoize?` | The optional memoize function that is used to memoize the arguments passed into the `output selector` generated by [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`).
**`Default`** `defaultMemoize` | | `options.argsMemoizeOptions?` | Optional configuration options for the `argsMemoize` function. These options are passed to the `argsMemoize` function as the second argument.
**`Since`** 5.0.0 | | `options.inputStabilityCheck?` | Overrides the global input stability check for the selector. Possible values are:
`once` - Run only the first time the selector is called.
`always` - Run every time the selector is called.
`never` - Never run the input stability check.
**`Default`** = `'once'`
**`Since`** 5.0.0 | -| `options.memoize` | The memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). **`Since`** 5.0.0 | +| `options.memoize` | The memoize function that is used to memoize the `resultFunc` inside [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). **`Since`** 5.0.0 | | `options.memoizeOptions?` | Optional configuration options for the `memoize` function. These options are passed to the `memoize` function as the second argument.
**`Since`** 5.0.0 |
-
- - - -#### Parameters - - +
Parameters | Name | Description | | :-------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| `memoize` | The `memoize` function responsible for memoizing the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). | +| `memoize` | The `memoize` function responsible for memoizing the `resultFunc` inside [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). | | `...memoizeOptionsFromArgs` | Optional configuration options for the memoization function. These options are then passed to the memoize function as the second argument onwards. |
-
+
Returns - - -#### Returns - - - -A customized `createSelector` function. +A customized [`createSelector`] function.
-
- - +
Type parameters -#### Type parameters - -
- -| Name | Description | -| :-------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `MemoizeFunction` | The type of the memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). | -| `ArgsMemoizeFunction` | The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used. | +| Name | Description | +| :-------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `MemoizeFunction` | The type of the memoize function that is used to memoize the `resultFunc` inside [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). | +| `ArgsMemoizeFunction` | The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used. |
-
- - - -#### **`Examples`** - - +
Examples ##### Using `options` (**`Since`** 5.0.0) @@ -1008,7 +849,7 @@ customSelector( ##### Using `memoize` and `...memoizeOptions` -`createSelectorCreator` can be used to make a customized version of `createSelector`. +`createSelectorCreator` can be used to make a customized version of [`createSelector`]. The `memoize` argument is a memoization function to replace `defaultMemoize`. @@ -1079,70 +920,40 @@ const selector = customSelectorCreator( ### createStructuredSelector({ inputSelectors }, selectorCreator = createSelector) -
- - - -#### Description - - +
Description A convenience function for a common pattern that arises when using `Reselect`. -The selector passed to a `connect` decorator often just takes the values of its `input selectors` +The selector passed to a `connect` decorator often just takes the values of its [`input selectors`] and maps them to keys in an object.
-
- - - -#### Parameters +
Parameters -
- -| Name | Description | -| :----------------- | :------------------------------------------------------------------- | -| `selectorMap` | A key value pair consisting of input selectors. | -| `selectorCreator?` | A custom selector creator function. It defaults to `createSelector`. | +| Name | Description | +| :----------------- | :--------------------------------------------------------------------- | +| `selectorMap` | A key value pair consisting of input selectors. | +| `selectorCreator?` | A custom selector creator function. It defaults to [`createSelector`]. |
-
- - - -#### Returns - - +
Returns A memoized structured selector.
-
- - - -#### Type parameters - - +
Type parameters | Name | Description | | :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `InputSelectorsObject` | The shape of the `input selectors` object. | +| `InputSelectorsObject` | The shape of the [`input selectors`] object. | | `MemoizeFunction` | The type of the memoize function that is used to create the structured selector. It defaults to `defaultMemoize`. | | `ArgsMemoizeFunction` | The type of the of the memoize function that is used to memoize the arguments passed into the generated structured selector. It defaults to `defaultMemoize`. |
-
- - - -#### **`Examples`** - - +
Examples ##### Modern Use Case @@ -1218,21 +1029,15 @@ const result = structuredSelector({ a: 1, b: 2 }) // will produce { x: 1, y: 2 } ## Debugging Tools -
- - - -### Development-only Checks - (**`Since`** 5.0.0) - - +
Development-only Checks - (Since 5.0.0) #### `inputStabilityCheck` -Due to how [`Cascading Memoization`](#cascadingmemoization) works in `Reselect`, it is crucial that your `input selectors` do not return a new reference on each run. If an `input selector` always returns a new reference, like +Due to how [`Cascading Memoization`][**_Cascading Memoization_**] works in `Reselect`, it is crucial that your [`input selectors`] do not return a new reference on each run. If an [`input selector`][`input selectors`] always returns a new reference, like ```ts state => ({ a: state.a, b: state.b }) @@ -1245,7 +1050,7 @@ state => state.todos.map(todo => todo.id) ``` that will cause the selector to never memoize properly. -Since this is a common mistake, we've added a development mode check to catch this. By default, `createSelector` will now run the `input selectors` twice during the first call to the selector. If the result appears to be different for the same call, it will log a warning with the arguments and the two different sets of extracted input values. +Since this is a common mistake, we've added a development mode check to catch this. By default, [`createSelector`] will now run the [`input selectors`] twice during the first call to the selector. If the result appears to be different for the same call, it will log a warning with the arguments and the two different sets of extracted input values. ```ts type StabilityCheckFrequency = 'always' | 'once' | 'never' @@ -1281,10 +1086,10 @@ setInputStabilityCheckEnabled('always') setInputStabilityCheckEnabled('never') ``` -##### 2. Per selector by passing an `inputStabilityCheck` option directly to `createSelector`: +##### 2. Per selector by passing an `inputStabilityCheck` option directly to [`createSelector`]: ```ts -// Create a selector that double-checks the results of `input selectors` every time it runs. +// Create a selector that double-checks the results of [`input selectors`][Input Selectors] every time it runs. const selectCompletedTodosLength = createSelector( [ // This `input selector` will not be memoized properly since it always returns a new reference. @@ -1302,30 +1107,24 @@ const selectCompletedTodosLength = createSelector(
-
- - - ### Output Selector Fields - - -The `output selectors` created by `createSelector` have several additional properties attached to them: - -| Name | Description | -| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `resultFunc` | The final function passed to `createSelector`. Otherwise known as the `combiner`. | -| `memoizedResultFunc` | The memoized version of `resultFunc`. | -| `lastResult` | Returns The last result calculated by `memoizedResultFunc`. | -| `dependencies` | The array of the input selectors used by `createSelector` to compose the combiner (`memoizedResultFunc`). | -| `recomputations` | Counts the number of times `memoizedResultFunc` has been recalculated. | -| `resetRecomputations` | Resets the count of `recomputations` count to 0. | -| `dependencyRecomputations` | Counts the number of times the input selectors (`dependencies`) have been recalculated. This is distinct from `recomputations`, which tracks the recalculations of the result function. | -| `resetDependencyRecomputations` | Resets the `dependencyRecomputations` count to 0. | -| `memoize` | Function used to memoize the `resultFunc`. | -| `argsMemoize` | Function used to memoize the arguments passed into the `output selector`. | +
The output selectors created by createSelector have several additional properties attached to them: + +| Name | Description | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `resultFunc` | The final function passed to [`createSelector`]. Otherwise known as the `combiner`. | +| `memoizedResultFunc` | The memoized version of `resultFunc`. | +| `lastResult` | Returns The last result calculated by `memoizedResultFunc`. | +| `dependencies` | The array of the input selectors used by [`createSelector`] to compose the combiner (`memoizedResultFunc`). | +| `recomputations` | Counts the number of times `memoizedResultFunc` has been recalculated. | +| `resetRecomputations` | Resets the count of `recomputations` count to 0. | +| `dependencyRecomputations` | Counts the number of times the [`input selectors`] ([`dependencies`]) have been recalculated. This is distinct from `recomputations`, which tracks the recalculations of the [`result function`]. | +| `resetDependencyRecomputations` | Resets the `dependencyRecomputations` count to 0. | +| `memoize` | Function used to memoize the `resultFunc`. | +| `argsMemoize` | Function used to memoize the arguments passed into the [`output selector`]. |
@@ -1333,27 +1132,21 @@ The `output selectors` created by `createSelector` have several additional prope --- -
- - - ## What's New in 5.0.0? - - -Version 5.0.0 introduces several new features and improvements: +
Version 5.0.0 introduces several new features and improvements: - **Customization Enhancements**: - - Added the ability to pass an options object to `createSelectorCreator`, allowing for customized `memoize` and `argsMemoize` functions, alongside their respective options (`memoizeOptions` and `argsMemoizeOptions`). - - The `createSelector` function now supports direct customization of `memoize` and `argsMemoize` within its options object. + - Added the ability to pass an options object to [`createSelectorCreator`], allowing for customized `memoize` and `argsMemoize` functions, alongside their respective options (`memoizeOptions` and `argsMemoizeOptions`). + - The [`createSelector`] function now supports direct customization of `memoize` and `argsMemoize` within its options object. - **Memoization Functions**: - Introduced new experimental memoization functions: `weakMapMemoize` and `autotrackMemoize`. - - Incorporated `memoize` and `argsMemoize` into the [`output selector fields`](#outputselectorfields) for debugging purposes. + - Incorporated `memoize` and `argsMemoize` into the [`output selector fields`] for debugging purposes. - **TypeScript Support and Performance**: @@ -1368,8 +1161,8 @@ Version 5.0.0 introduces several new features and improvements: - **Additional Functionalities**: - - Added `dependencyRecomputations` and `resetDependencyRecomputations` to the `output selector fields`. These additions provide greater control and insight over `input selectors`, complementing the new `argsMemoize` API. - - Introduced `inputStabilityCheck`, a development tool that runs the `input selectors` twice using the same arguments and triggers a warning If they return differing results for the same call. + - Added `dependencyRecomputations` and `resetDependencyRecomputations` to the [`output selector fields`]. These additions provide greater control and insight over [`input selectors`], complementing the new `argsMemoize` API. + - Introduced `inputStabilityCheck`, a development tool that runs the [`input selectors`] twice using the same arguments and triggers a warning If they return differing results for the same call. These updates aim to enhance flexibility, performance, and developer experience. For detailed usage and examples, refer to the updated documentation sections for each feature. @@ -1379,24 +1172,57 @@ These updates aim to enhance flexibility, performance, and developer experience. --- -
- - - ## Optimizing `Reselect` - +### Common Mistakes + +
Click to expand + +A somewhat common mistake is to write an [`input selector`][`input selectors`] that extracts a value or does some derivation, and a [`result function`] that just returns its result: + +```ts +// ❌ BROKEN: this will not memoize correctly, and does nothing useful! +const brokenSelector = createSelector( + [(state: RootState) => state.todos], + todos => todos +) +``` + +Any [`result function`] that just returns its inputs is incorrect! The [`result function`] should always have the transformation logic. + +Similarly: + +```ts +// ❌ BROKEN: this will not memoize correctly! +const brokenSelector = createSelector( + [(state: RootState) => state], + state => state.todos +) +``` + +
+ +### Handling Empty Array Results -### Empty Array Pattern +
Click to expand To reduce recalculations, use a predefined empty array when `array.filter` or similar methods result in an empty array. So you can have a pattern like this: ```ts -const EMPTY_ARRAY = [] +interface RootState { + todos: { + id: number + title: string + description: string + completed: boolean + }[] +} + +const EMPTY_ARRAY: [] = [] const selectCompletedTodos = createSelector( [(state: RootState) => state.todos], @@ -1410,7 +1236,7 @@ const selectCompletedTodos = createSelector( Or to avoid repetition, you can create a wrapper function and reuse it: ```ts -const EMPTY_ARRAY = [] +const EMPTY_ARRAY: [] = [] export const fallbackToEmptyArray = (array: T[]) => { return array.length === 0 ? EMPTY_ARRAY : array @@ -1424,16 +1250,33 @@ const selectCompletedTodos = createSelector( ) ``` +This way if the [`result function`] returns an empty array twice in a row, your component will not re-render due to a stable empty array reference: + +```ts +const completedTodos = selectCompletedTodos(store.getState()) + +store.dispatch(addTodo()) + +console.log(completedTodos === selectCompletedTodos(store.getState())) //=> true +``` + +
+ +### Best Practices + +
Click to expand + There are a few details that will help you skip running as many functions as possible and get the best possible performance out of `Reselect`: -- Due to the `Cascading Memoization` in `Reselect`, The first layer of checks is upon the arguments that are passed to the `output selector`, therefore it's best to maintain the same reference for the arguments as much as possible. -- In `Redux`, your state will change reference when updated. But it's best to keep the additional arguments as simple as possible, try to avoid passing in objects or arrays and mostly stick to primitives like numbers for ids. -- Keep your [`input selectors`](#input-selectors) as simple as possible. It's best if they mostly consist of field accessors like `state => state.todos` or argument providers like `(state, id) => id`. You should not be doing any sort of calculation inside `input selectors`, and you should definitely not be returning an object or array with a new reference each time. -- The `result function` is only re-run as a last resort. So make sure to put any and all calculations inside your `result function`. That way, `Reselect` will only run those calculations if all other checks fail. +- Due to the [`Cascading Memoization`][**_Cascading Memoization_**] in `Reselect`, The first layer of checks is upon the arguments that are passed to the [`output selector`], therefore it's best to maintain the same reference for the arguments as much as possible. +- In `Redux`, your state will change reference when updated. But it's best to keep the additional arguments as simple as possible, you can pass in objects or array as long as their reference does not change. Or you can pass in primitives like numbers for ids. +- Keep your [`input selectors`] as simple as possible. It's best if they mostly consist of field accessors like `state => state.todos` or argument providers like `(state, id) => id`. You should not be doing any sort of calculation inside [`input selectors`], and you should definitely not be returning an object or array with a new reference each time. +- The [`result function`] is only re-run as a last resort. So make sure to put any and all calculations inside your [`result function`]. That way, `Reselect` will only run those calculations if all other checks fail. This: ```ts +// ✔️ This is optimal because we have less calculations in [`input selectors`][Input Selectors] and more in the `result function`. const selectorGood = createSelector( [(state: RootState) => state.todos], todos => someExpensiveComputation(todos) @@ -1443,43 +1286,79 @@ const selectorGood = createSelector( Is preferable to this: ```ts +// ❌ This is not optimal! const selectorBad = createSelector( [(state: RootState) => someExpensiveComputation(state.todos)], someOtherCalculation ) ``` -Because we have less calculations in input selectors and more in the result function. -
--- -## FAQ +## Additional Tips & Tricks -
+
Top Level Selectors Pattern - +This pattern simplifies the creation of selectors in your application. Start by defining a generic type for your top-level selectors: -### Why isn’t my selector recomputing when the input state changes? +```ts +import type { Selector } from 'reselect' - +export type TopLevelSelectors = { + [K in keyof State as K extends string + ? `select${Capitalize}` + : never]: Selector +} +``` + +With this setup, you can easily create top-level selectors for your application's state: + +```ts +const topLevelSelectors: TopLevelSelectors = { + selectAlerts: state => state.alerts, + selectTodos: state => state.todos, + selectUsers: state => state.users +} +``` + +This approach allows for more streamlined and readable selector creation. For example: -A: Check that your memoization function is compatible with your state update function (i.e. the reducer if you are using `Redux`). For example, a selector created with `createSelector` will not work with a state update function that mutates an existing object instead of creating a new one each time. `createSelector` uses an identity check (`===`) to detect that an input has changed, so mutating an existing object will not trigger the selector to recompute because mutating an object does not change its identity. Note that if you are using `Redux`, mutating the state object is [almost certainly a mistake](http://redux.js.org/docs/Troubleshooting.html). +```ts +const selectCompletedTodos = createSelector( + [topLevelSelectors.selectTodos], + todos => todos.filter(todo => todo.completed === true) +) +```
-
+ + +--- + +## FAQ + +### Why isn’t my selector recomputing when the input state changes? - +
Answer + +Check that your memoization function is compatible with your state update function (i.e. the reducer if you are using `Redux`). For example, a selector created with [`createSelector`] will not work with a state update function that mutates an existing object instead of creating a new one each time. [`createSelector`] uses an identity check (`===`) to detect that an input has changed, so mutating an existing object will not trigger the selector to recompute because mutating an object does not change its identity. Note that if you are using `Redux`, mutating the state object is [almost certainly a mistake](http://redux.js.org/docs/Troubleshooting.html). + +
### Why is my selector recomputing when the input state stays the same? -
+
Answer -A: Make sure you have `inputStabilityCheck` set to either `always` or `once` and that in and of itself should take some weight off of your shoulders by doing some of the debugging for you. Also make sure you use your `output selector fields` like `recomputations`, `resetRecomputations`, `dependencyRecomputations`, `resetDependencyRecomputations` to narrow down the root of the problem to see where it is coming from. Is it coming from the arguments changing reference unexpectedly? then if that is the case your `dependencyRecomputations` should be going up. If `dependencyRecomputations` is incrementing but `recomputations` is not, that means your arguments are changing reference too often. If your `input selectors` return a new reference every time, that will be caught be `inputStabilityCheck`. And if your arguments are changing reference too often, you can narrow it down to see which arguments are changing reference too often by doing this: +To address unexpected recomputations in your selector, first ensure that `inputStabilityCheck` is set to either `'always'` or `'once'`. This setting aids in debugging by monitoring the stability of your inputs. Additionally, utilize [`output selector fields`] such as `recomputations`, `resetRecomputations`, `dependencyRecomputations`, and `resetDependencyRecomputations`. These tools help identify the source of the issue. + +Keep an eye on the `dependencyRecomputations` count. If it increases while `recomputations` remains the same, it suggests that your arguments are changing references but your [`input selectors`] are stable which is typically the desired behavior. + +To delve deeper, you can determine which arguments are changing references too frequently by using the `argsMemoizeOptions` and `equalityCheck`. Consider the following example: ```ts interface RootState { @@ -1498,7 +1377,7 @@ const selectAlertsByType = createSelector( // This will check the arguments passed to the `output selector`. equalityCheck: (a, b) => { if (a !== b) { - console.log(a, 'is not equal to', b) + console.log('Changed argument:', a, 'to', b) } return a === b } @@ -1509,48 +1388,33 @@ const selectAlertsByType = createSelector(
-
- - - ### Can I use `Reselect` without `Redux`? - +
Answer -A: Yes. `Reselect` has no dependencies on any other package, so although it was designed to be used with `Redux` it can be used independently. It can be used with any plain JS data, such as typical `React` state values, as long as that data is being updated immutably. +Yes. `Reselect` has no dependencies on any other package, so although it was designed to be used with `Redux` it can be used independently. It can be used with any plain JS data, such as typical `React` state values, as long as that data is being updated immutably.
-
+### How do I create a selector that takes an argument? - +
Answer -### Q: How do I create a selector that takes an argument? +When creating a selector that accepts arguments in `Reselect`, it's important to structure your input and [`output selectors`][Output Selectors] appropriately. Here are key points to consider: -
+1. **Consistency in Arguments**: Ensure that all positional arguments across [`input selectors`] are of the same type for consistency. -Conceptually, Reselect works like this internally: - -```ts -const finalSelector = (...args) => { - const extractedValues = inputFunctions.map(input => input(...args)) - return output(...extractedValues) -} -``` - -In other words, all the arguments passed to the selector function are immediately passed to all of the inputs. - -When creating a selector that accepts arguments in `Reselect`, it's important to structure your input and `output selectors` appropriately. Here are key points to consider: - -1. **Consistency in Arguments**: Ensure that all positional arguments across `input selectors` are of the same type for consistency. - -2. **Selective Argument Usage**: Design each selector to use only its relevant argument(s) and ignore the rest. This is crucial because all `input selectors` receive the same arguments that are passed to the `output selector`. +2. **Selective Argument Usage**: Design each selector to use only its relevant argument(s) and ignore the rest. This is crucial because all [`input selectors`] receive the same arguments that are passed to the [`output selector`][Output Selectors]. Suppose we have the following state structure: ```ts interface RootState { - items: { id: number; category: string }[] + items: { + id: number + category: string + vendor: { id: number; name: string } + }[] // ... other state properties ... } ``` @@ -1588,25 +1452,45 @@ const finalResult = items.filter(item => item.category === category && item.id !== id) ``` -More generally, you can have N arguments passed to the selector, and you can have M input functions extracting values from any of those arguments. All M extracted values get passed to the output function. +
Additional Examples -### The default memoization function is no good, can I use a different one? +```ts +const selectItems = (state: RootState) => state.items - +// expects a number as the second argument +const selectItemId = (state: RootState, itemId: number) => itemId -A: We think it works great for a lot of use cases, but sure. See [these examples](#customize-equalitycheck-for-defaultmemoize). +// expects an object as the second argument +const selectVendorName = ( + state: RootState, + vendor: { id: number; name: string } +) => vendor.name + +const selectItemById = createSelector( + [selectItems, selectItemId, selectVendorName], + (items, itemId, vendorName) => items[itemId] +) +``` + +In this example, `selectItemId` expects that its second argument will be some simple value, while `selectVendorName` expects that the second argument is an object. If you call `selectItemById(state, 42)`, `selectVendorName` will break because it's trying to access `42.name`.
-
+
+ +### The default memoization function is no good, can I use a different one? + +
Answer - +We think it works great for a lot of use cases, but sure. See [these examples](#customize-equalitycheck-for-defaultmemoize). + +
### How do I test a selector? - +
Answer -A: For a given input, a selector should always produce the same result. For this reason they are simple to unit test. +For a given input, a selector should always produce the same result. For this reason they are simple to unit test. ```ts interface RootState { @@ -1625,7 +1509,7 @@ const state: RootState = { ] } -// With `Vitest` +// With `Vitest` or `Jest` test('selector unit test', () => { const selectTodoIds = createSelector( [(state: RootState) => state.todos], @@ -1642,60 +1526,65 @@ test('selector unit test', () => { selectTodoIds(state) // The `Result Function` should not recalculate. expect(selectTodoIds.recomputations()).toBe(1) - // `Input selectors` should not recalculate. + // `input selectors` should not recalculate. expect(selectTodoIds.dependencyRecomputations()).toBe(1) }) + +// With `Chai` +test('selector unit test', () => { + const selectTodoIds = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id) + ) + const firstResult = selectTodoIds(state) + const secondResult = selectTodoIds(state) + // Reference equality should pass. + expect(firstResult).to.equal(secondResult) + // Deep equality should also pass. + expect(firstResult).to.deep.equal(secondResult) + selectTodoIds(state) + selectTodoIds(state) + selectTodoIds(state) + // The `result function` should not recalculate. + expect(selectTodoIds.recomputations()).to.equal(1) + // `input selectors` should not recalculate. + expect(selectTodoIds.dependencyRecomputations()).to.equal(1) +}) ```
-
- - - ### Can I share a selector across multiple component instances? - +
Answer -A: Yes, as of 5.0.0 you can use `weakMapMemoize` to achieve this. +Yes, as of 5.0.0 you can use [`weakMapMemoize`](#weakmapmemoizefunc---since-500) to achieve this.
-
- - - ### Are there TypeScript Typings? - +
Answer -A: Yes! `Reselect` is now written in `TypeScript` itself, so they should Just Work™. +Yes! `Reselect` is now written in `TypeScript` itself, so they should Just Work™.
-
- - - ### I am seeing a TypeScript error: `Type instantiation is excessively deep and possibly infinite` - +
Answer -A: **`Since`** 5.0.0 you should be able to nest up to 30 selectors, but in case you still run into this issue, you can refer to [this +**`Since`** 5.0.0 you should be able to nest up to 30 selectors, but in case you still run into this issue, you can refer to [this comment](https://github.com/reduxjs/reselect/issues/534#issuecomment-956708953) for a discussion of the problem, as relating to nested selectors.
-
- - - ### How can I make a [curried](https://github.com/hemanth/functional-programming-jargon#currying) selector? - +
Answer -A: You can try this new experimental API: +You can try this new experimental API: @@ -1726,16 +1615,16 @@ curriedSelector(0)(state) As before you had to do this: ```ts -const selectTodo = useSelector(state => parametricSelector(state, props.id)) +const selectTodo = useSelector(state => parametricSelector(state, id)) ``` Now you can do this: ```ts -const selectTodo = useSelector(curriedSelector(props.id)) +const selectTodo = useSelector(curriedSelector(id)) ``` -Another thing you can do if you are using `React-Redux` is this: +Another thing you can do if you are using `React-Redux` is create a custom hook factory function: ```ts import type { GetParamsFromSelectors, Selector } from 'reselect' @@ -1767,69 +1656,101 @@ const MyComponent: FC = ({ id }) => { } ``` -### How can I make pre-typed version of `createSelector` for my root state? +
+ +### How can I make pre-typed version of [`createSelector`](#createselectorinputselectors--inputselectors-resultfunc-createselectoroptions) for my root state? + +
Answer -A: You can create a custom typed version of `createSelector` by defining a utility type that extends the original `createSelector` function. Here's an example: +You can create a custom typed version of [`createSelector`] by defining a utility type that extends the original [`createSelector`] function. Here's an example: ```ts -import type { createSelector, SelectorsArray, Selector } from 'reselect' +import type { SelectorsArray, Selector } from 'reselect' +import { createSelector } from 'reselect' interface RootState { todos: { id: number; completed: boolean }[] alerts: { id: number; read: boolean }[] } -export type TypedCreateSelector = < - SelectorsArray extends readonly Selector[], - Result +export type TypedCreateSelector< + State, + MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize +> = < + InputSelectors extends readonly Selector[], + Result, + OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction, + OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction >( ...createSelectorArgs: Parameters< - typeof createSelector + typeof createSelector< + InputSelectors, + Result, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > > -) => ReturnType> +) => ReturnType< + typeof createSelector< + InputSelectors, + Result, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > +> export const createAppSelector: TypedCreateSelector = createSelector ``` -> [!WARNING]: This approach currently only supports `input selectors` provided as a single array. +> [!WARNING]: This approach currently only supports [`input selectors`] provided as a single array.
- +### What if I want to use [`createSelector`](#createselectorinputselectors--inputselectors-resultfunc-createselectoroptions) without memoization? ---- +
Answer -
+Create an [`identity function`][Identity Function]: - +```ts +const identity = any>(func: Func) => func -## External References +const createNonMemoizedSelector = createSelectorCreator({ + memoize: identity, + argsMemoize: identity +}) +``` - +
-- [**`WeakMap`**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) -- [**`Reference Equality Check`**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality) + -
+--- -
+## External References + +
Click to expand - +- [**`WeakMap`**][`WeakMap`] +- [**`Reference Equality Check`**][Reference Equality Check] +- [**`Memoization`**][Memoization] +- [**`Identity Function`**][Identity Function] + +
## Related Projects - +
Click to expand ### [re-reselect](https://github.com/toomuchdesign/re-reselect) -Enhances `Reselect` selectors by wrapping `createSelector` and returning a memoized collection of selectors indexed with the cache key returned by a custom resolver function. +Enhances `Reselect` selectors by wrapping [`createSelector`] and returning a memoized collection of selectors indexed with the cache key returned by a custom resolver function. Useful to reduce selectors recalculation when the same selector is repeatedly called with one/few different arguments. ### [reselect-tools](https://github.com/skortchmark9/reselect-tools) -[Chrome extension](https://chrome.google.com/webstore/detail/reselect-devtools/cjmaipngmabglflfeepmdiffcijhjlbb?hl=en) and [companion lib](https://github.com/skortchmark9/reselect-tools) for debugging selectors. - - Measure selector recomputations across the app and identify performance bottlenecks - Check selector dependencies, inputs, outputs, and recomputations at any time with the chrome extension - Statically export a JSON representation of your selector graph for further analysis @@ -1858,13 +1779,9 @@ Inspired by `Reselect Tools`, so it also has all functionality from this library MIT -
- - - ## Prior Art and Inspiration - +
Click to expand Originally inspired by getters in [NuclearJS](https://github.com/optimizely/nuclear-js.git), [subscriptions](https://github.com/Day8/re-frame#just-a-read-only-cursor) in [re-frame](https://github.com/Day8/re-frame) and this [proposal](https://github.com/reduxjs/redux/pull/169) from [speedskater](https://github.com/speedskater). @@ -1876,6 +1793,34 @@ Originally inspired by getters in [NuclearJS](https://github.com/optimizely/nucl [coveralls-badge]: https://img.shields.io/coveralls/reduxjs/reselect/master.svg?style=for-the-badge [coveralls]: https://coveralls.io/github/reduxjs/reselect + + +[`WeakMap`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap 'WeakMap' +[Reference Equality Check]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality 'Reference Equality Check' +[Memoization]: https://en.wikipedia.org/wiki/Memoization 'Memoization' +[Identity Function]: https://en.wikipedia.org/wiki/Identity_function 'Identity Function' +[`useMemo`]: https://react.dev/reference/react/useMemo#usememo 'useMemo' +[`useCallback`]: https://react.dev/reference/react/useCallback#usecallback 'useCallback' +[`re-reselect`]: https://github.com/toomuchdesign/re-reselect 're-reselect' + + + +[`selector`]: #selector-function 'Selector Function' +[`input selectors`]: #input-selectors 'Input Selectors' +[`output selector`]: #output-selector 'Output Selector' +[`result function`]: #result-function 'Result Function' +[`combiner`]: #combiner 'Combiner' +[`dependencies`]: #dependencies 'Dependencies' +[**_Cascading Memoization_**]: #cascading-memoization 'Cascading Memoization' +[`output selector fields`]: #output-selector-fields 'Output Selector Fields' +[`createSelector`]: #createselectorinputselectors--inputselectors-resultfunc-createselectoroptions 'createSelector' +[`createSelectorCreator`]: #createselectorcreatormemoize--options-memoizeoptions 'createSelectorCreator' +[`defaultMemoize`]: #defaultmemoizefunc-equalitycheckoroptions--defaultequalitycheck 'defaultMemoize' +[`weakMapMemoize`]: #weakmapmemoizefunc---since-500 'weakMapMemoize' +[`autotrackMemoize`]: #autotrackmemoizefunc---since-500 'autotrackMemoize' +[`createStructuredSelector`]: #createstructuredselector-inputselectors--selectorcreator--createselector 'createStructuredSelector' +[`createCurriedSelector`]: #createcurriedselectorinputselectors--inputselectors-resultfunc-createselectoroptions 'createCurriedSelector' +
From 7e222906ce126252a176a9e535e38accae93b4fc Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Mon, 13 Nov 2023 10:39:00 -0600 Subject: [PATCH 16/50] Fix `TypedCreateSelector` in `README` - Add `examples.test.ts` file to test examples from docs. - Fix remaining benchmarks. --- README.md | 7 +- .../benchmarks/createCurriedSelector.bench.ts | 12 +- test/benchmarks/orderOfExecution.bench.ts | 96 ++++++++--- test/benchmarks/reselect.bench.ts | 124 ++++++++------ test/benchmarks/weakMapMemoize.bench.ts | 47 +++-- test/examples.test.ts | 161 ++++++++++++++++++ 6 files changed, 338 insertions(+), 109 deletions(-) create mode 100644 test/examples.test.ts diff --git a/README.md b/README.md index 52e8860d0..b78a73510 100644 --- a/README.md +++ b/README.md @@ -1665,7 +1665,12 @@ const MyComponent: FC = ({ id }) => { You can create a custom typed version of [`createSelector`] by defining a utility type that extends the original [`createSelector`] function. Here's an example: ```ts -import type { SelectorsArray, Selector } from 'reselect' +import type { + OutputSelector, + Selector, + SelectorArray, + UnknownMemoizer +} from 'reselect' import { createSelector } from 'reselect' interface RootState { diff --git a/test/benchmarks/createCurriedSelector.bench.ts b/test/benchmarks/createCurriedSelector.bench.ts index 428e6f6d9..aab34bea6 100644 --- a/test/benchmarks/createCurriedSelector.bench.ts +++ b/test/benchmarks/createCurriedSelector.bench.ts @@ -4,10 +4,10 @@ import { bench } from 'vitest' import type { RootState } from '../testUtils' import { setFunctionNames, setupStore } from '../testUtils' -describe.only('curriedSelector vs parametric selector', () => { - const options: Options = { - // iterations: 10_000_000, - // time: 0 +describe('curriedSelector vs parametric selector', () => { + const commonOptions: Options = { + iterations: 10, + time: 0 } const store = setupStore() const state = store.getState() @@ -26,7 +26,7 @@ describe.only('curriedSelector vs parametric selector', () => { parametricSelector(state, 0) }, { - ...options, + ...commonOptions, setup: (task, mode) => { if (mode === 'warmup') return parametricSelector.clearCache() @@ -41,7 +41,7 @@ describe.only('curriedSelector vs parametric selector', () => { curriedSelector(0)(state) }, { - ...options, + ...commonOptions, setup: (task, mode) => { if (mode === 'warmup') return curriedSelector.clearCache() diff --git a/test/benchmarks/orderOfExecution.bench.ts b/test/benchmarks/orderOfExecution.bench.ts index db156ba38..7e0b76642 100644 --- a/test/benchmarks/orderOfExecution.bench.ts +++ b/test/benchmarks/orderOfExecution.bench.ts @@ -1,5 +1,5 @@ -import type { Selector } from 'reselect' -import { createSelector } from 'reselect' +import type { OutputSelector, Selector } from 'reselect' +import { createSelector, defaultMemoize } from 'reselect' import type { Options } from 'tinybench' import { bench } from 'vitest' import type { RootState } from '../testUtils' @@ -10,9 +10,8 @@ import { toggleCompleted } from '../testUtils' -describe.only('less in input selectors vs more in input selectors', () => { +describe('less in input selectors vs more in input selectors', () => { const store = setupStore() - const state = store.getState() const arr = Array.from({ length: 1_000_000 }, (e, i) => i) const runSelector = (selector: Selector) => { arr.forEach((e, i) => { @@ -39,28 +38,18 @@ describe.only('less in input selectors vs more in input selectors', () => { called++ return state.todos.find(todo => todo.id === id)?.completed } - const options: Options = { - // warmupIterations: 0, - // warmupTime: 0, + const commonOptions: Options = { iterations: 10, time: 0 } setFunctionNames({ selectorGood, selectorBad, nonMemoized }) - const createOptions = < - S extends Selector & { - recomputations: () => number - dependencyRecomputations: () => number - } - >( - selector: S - ) => { + const createOptions = (selector: S) => { const options: Options = { setup: (task, mode) => { if (mode === 'warmup') return task.opts = { beforeEach: () => { store.dispatch(toggleCompleted(1)) - // store.dispatch(toggleRead(0)) }, afterAll: () => { logRecomputations(selector) @@ -76,7 +65,7 @@ describe.only('less in input selectors vs more in input selectors', () => { selectorGood(store.getState(), 0) }, { - ...options, + ...commonOptions, ...createOptions(selectorGood) } ) @@ -86,7 +75,7 @@ describe.only('less in input selectors vs more in input selectors', () => { selectorBad(store.getState(), 0) }, { - ...options, + ...commonOptions, ...createOptions(selectorBad) } ) @@ -96,7 +85,7 @@ describe.only('less in input selectors vs more in input selectors', () => { nonMemoized(store.getState(), 0) }, { - ...options, + ...commonOptions, setup: (task, mode) => { if (mode === 'warmup') { called = 0 @@ -104,7 +93,6 @@ describe.only('less in input selectors vs more in input selectors', () => { } task.opts = { beforeEach: () => { - // store.dispatch(toggleRead(0)) store.dispatch(toggleCompleted(1)) }, afterAll: () => { @@ -115,3 +103,71 @@ describe.only('less in input selectors vs more in input selectors', () => { } ) }) + +describe('using standalone memoization methods vs createSelector', () => { + const store = setupStore() + const commonOptions: Options = { + iterations: 10, + time: 0 + } + const fieldAccessor = createSelector( + [(state: RootState) => state.users], + users => users.appSettings + ) + let called = 0 + const fieldAccessor1 = defaultMemoize((state: RootState) => { + called++ + return state.users.appSettings + }) + setFunctionNames({ fieldAccessor, fieldAccessor1 }) + const createOptions = < + S extends Selector & { + recomputations: () => number + dependencyRecomputations: () => number + } + >( + selector: S + ) => { + const options: Options = { + setup: (task, mode) => { + if (mode === 'warmup') return + task.opts = { + beforeEach: () => { + store.dispatch(toggleCompleted(1)) + }, + afterAll: () => { + logRecomputations(selector) + } + } + } + } + return options + } + bench( + fieldAccessor, + () => { + fieldAccessor(store.getState()) + }, + { ...commonOptions, ...createOptions(fieldAccessor) } + ) + bench( + fieldAccessor1, + () => { + fieldAccessor1(store.getState()) + }, + { + ...commonOptions, + setup: (task, mode) => { + if (mode === 'warmup') return + task.opts = { + beforeEach: () => { + store.dispatch(toggleCompleted(1)) + }, + afterAll: () => { + console.log(fieldAccessor1.name, called) + } + } + } + } + ) +}) diff --git a/test/benchmarks/reselect.bench.ts b/test/benchmarks/reselect.bench.ts index 0b69bd464..6cc95ce7f 100644 --- a/test/benchmarks/reselect.bench.ts +++ b/test/benchmarks/reselect.bench.ts @@ -8,112 +8,122 @@ import { bench } from 'vitest' import type { RootState } from '../testUtils' import { setFunctionNames, setupStore } from '../testUtils' -const options: Options = { - // iterations: 10_000_000, - // time: 0 -} - -describe.skip('bench', () => { +describe('general benchmark', () => { + const commonOptions: Options = { + iterations: 10, + time: 0 + } const store = setupStore() const state = store.getState() const selectorDefault = createSelector( - (state: RootState) => state.todos, + [(state: RootState) => state.todos], todos => todos.map(({ id }) => id) ) const selectorAutotrack = createSelector( - (state: RootState) => state.todos, + [(state: RootState) => state.todos], todos => todos.map(({ id }) => id), { memoize: autotrackMemoize } ) const selectorWeakMap = createSelector( - (state: RootState) => state.todos, + [(state: RootState) => state.todos], todos => todos.map(({ id }) => id), { memoize: weakMapMemoize } ) const selectorArgsAutotrack = createSelector( - (state: RootState) => state.todos, + [(state: RootState) => state.todos], todos => todos.map(({ id }) => id), { argsMemoize: autotrackMemoize } ) const nonMemoizedSelector = (state: RootState) => { return state.todos.map(({ id }) => id) } - const selectorArgsWeakMap = createSelector( - (state: RootState) => state.todos, + [(state: RootState) => state.todos], todos => todos.map(({ id }) => id), { argsMemoize: weakMapMemoize } ) const parametricSelector = createSelector( - (state: RootState) => state.todos, - (state: RootState, id: number) => id, + [(state: RootState) => state.todos, (state: RootState, id: number) => id], (todos, id) => todos[id] ) const parametricSelectorWeakMapArgs = createSelector( - (state: RootState) => state.todos, - (state: RootState, id: number) => id, + [(state: RootState) => state.todos, (state: RootState, id: number) => id], (todos, id) => todos[id], { argsMemoize: weakMapMemoize } ) + setFunctionNames({ + selectorDefault, + selectorAutotrack, + selectorWeakMap, + selectorArgsAutotrack, + nonMemoizedSelector, + selectorArgsWeakMap, + parametricSelector, + parametricSelectorWeakMapArgs + }) bench( - 'selectorDefault', + selectorDefault, () => { selectorDefault(state) }, - options + commonOptions ) bench( - 'selectorAutotrack', + selectorAutotrack, () => { selectorAutotrack(state) }, - options + commonOptions ) bench( - 'selectorWeakMap', + selectorWeakMap, () => { selectorWeakMap(state) }, - options + commonOptions ) bench( - 'selectorArgsAutotrack', + selectorArgsAutotrack, () => { selectorArgsAutotrack(state) }, - options + commonOptions ) bench( - 'selectorArgsWeakMap', + selectorArgsWeakMap, () => { selectorArgsWeakMap(state) }, - options + commonOptions ) bench( - 'non-memoized selector', + nonMemoizedSelector, () => { nonMemoizedSelector(state) }, - options + commonOptions ) bench( - 'parametricSelector', + parametricSelector, () => { parametricSelector(state, 0) }, - options + commonOptions ) bench( - 'parametricSelectorWeakMapArgs', + parametricSelectorWeakMapArgs, () => { parametricSelectorWeakMapArgs(state, 0) }, - options + commonOptions ) }) -describe.skip('for loops', () => { +describe('for loops', () => { + const commonOptions: Options = { + iterations: 10, + time: 0 + } const store = setupStore() const state = store.getState() const { todos } = state @@ -127,7 +137,7 @@ describe.skip('for loops', () => { todos[i].id } }, - options + commonOptions ) bench( 'for loop length cached', @@ -138,7 +148,7 @@ describe.skip('for loops', () => { todos[i].id } }, - options + commonOptions ) bench( 'for loop length and arg cached', @@ -150,11 +160,15 @@ describe.skip('for loops', () => { arg.id } }, - options + commonOptions ) }) -describe.skip('nested field access', () => { +describe.todo('nested field access', () => { + const commonOptions: Options = { + iterations: 10, + time: 0 + } const store = setupStore() const state = store.getState() const selectorDefault = createSelector( @@ -172,25 +186,29 @@ describe.skip('nested field access', () => { () => { selectorDefault(state) }, - options + commonOptions ) bench( 'nonMemoizedSelector', () => { nonMemoizedSelector(state) }, - options + commonOptions ) bench( 'selectorDefault1', () => { selectorDefault1(state) }, - options + commonOptions ) }) -describe.skip('simple field access', () => { +describe.todo('simple field access', () => { + const commonOptions: Options = { + iterations: 10, + time: 0 + } const store = setupStore() const state = store.getState() const selectorDefault = createSelector( @@ -236,57 +254,55 @@ describe.skip('simple field access', () => { () => { selectorDefault(state) }, - options + commonOptions ) bench( 'nonMemoizedSelector', () => { nonMemoizedSelector(state) }, - options + commonOptions ) bench( 'selectorDefault1', () => { selectorDefault1(state) }, - options + commonOptions ) bench( 'selectorDefault2', () => { selectorDefault2(state) }, - options + commonOptions ) }) -describe.only('field accessors', () => { +describe.todo('field accessors', () => { + const commonOptions: Options = { + iterations: 10, + time: 0 + } const store = setupStore() const selectorDefault = createSelector( [(state: RootState) => state.users], users => users.appSettings ) const nonMemoizedSelector = (state: RootState) => state.users.appSettings - setFunctionNames({ selectorDefault, nonMemoizedSelector }) - - const options: Options = { - // iterations: 1000, - // time: 0 - } bench( selectorDefault, () => { selectorDefault(store.getState()) }, - { ...options } + { ...commonOptions } ) bench( nonMemoizedSelector, () => { nonMemoizedSelector(store.getState()) }, - { ...options } + { ...commonOptions } ) }) diff --git a/test/benchmarks/weakMapMemoize.bench.ts b/test/benchmarks/weakMapMemoize.bench.ts index f3d523635..7566613d0 100644 --- a/test/benchmarks/weakMapMemoize.bench.ts +++ b/test/benchmarks/weakMapMemoize.bench.ts @@ -14,12 +14,12 @@ const store = setupStore() const state = store.getState() const arr = Array.from({ length: 30 }, (e, i) => i) -const options: Options = { - // iterations: 100_000, - // time: 0 +const commonOptions: Options = { + iterations: 10, + time: 0 } -describe.only('weakMapMemoize vs defaultMemoize', () => { +describe('weakMapMemoize vs defaultMemoize', () => { const selectorDefault = createSelector( [(state: RootState) => state.todos, (state: RootState, id: number) => id], (todos, id) => todos.map(todo => todo.id === id) @@ -104,7 +104,7 @@ describe.only('weakMapMemoize vs defaultMemoize', () => { runSelector(selectorDefault) }, { - ...options, + ...commonOptions, setup: (task, mode) => { if (mode === 'warmup') return selectorDefault.clearCache() @@ -127,7 +127,7 @@ describe.only('weakMapMemoize vs defaultMemoize', () => { runSelector(selectorDefaultWithCacheSize) }, { - ...options, + ...commonOptions, setup: (task, mode) => { if (mode === 'warmup') return selectorDefaultWithCacheSize.clearCache() @@ -150,7 +150,7 @@ describe.only('weakMapMemoize vs defaultMemoize', () => { runSelector(selectorDefaultWithArgsCacheSize) }, { - ...options, + ...commonOptions, setup: (task, mode) => { if (mode === 'warmup') return selectorDefaultWithArgsCacheSize.clearCache() @@ -173,7 +173,7 @@ describe.only('weakMapMemoize vs defaultMemoize', () => { runSelector(selectorDefaultWithBothCacheSize) }, { - ...options, + ...commonOptions, setup: (task, mode) => { if (mode === 'warmup') return selectorDefaultWithBothCacheSize.clearCache() @@ -196,7 +196,7 @@ describe.only('weakMapMemoize vs defaultMemoize', () => { runSelector(selectorWeakMap) }, { - ...options, + ...commonOptions, setup: (task, mode) => { if (mode === 'warmup') return selectorWeakMap.clearCache() @@ -220,7 +220,7 @@ describe.only('weakMapMemoize vs defaultMemoize', () => { runSelector(selectorArgsWeakMap) }, { - ...options, + ...commonOptions, setup: (task, mode) => { if (mode === 'warmup') return selectorArgsWeakMap.clearCache() @@ -244,7 +244,7 @@ describe.only('weakMapMemoize vs defaultMemoize', () => { runSelector(selectorBothWeakMap) }, { - ...options, + ...commonOptions, setup: (task, mode) => { if (mode === 'warmup') return selectorBothWeakMap.clearCache() @@ -267,7 +267,7 @@ describe.only('weakMapMemoize vs defaultMemoize', () => { runSelector(selectorAutotrack) }, { - ...options, + ...commonOptions, setup: (task, mode) => { if (mode === 'warmup') return selectorAutotrack.clearCache() @@ -290,7 +290,7 @@ describe.only('weakMapMemoize vs defaultMemoize', () => { runSelector(selectorArgsAutotrack) }, { - ...options, + ...commonOptions, setup: (task, mode) => { if (mode === 'warmup') return selectorArgsAutotrack.clearCache() @@ -313,7 +313,7 @@ describe.only('weakMapMemoize vs defaultMemoize', () => { runSelector(selectorBothAutotrack) }, { - ...options, + ...commonOptions, setup: (task, mode) => { if (mode === 'warmup') return selectorBothAutotrack.clearCache() @@ -335,11 +335,11 @@ describe.only('weakMapMemoize vs defaultMemoize', () => { () => { runSelector(nonMemoizedSelector) }, - { ...options } + { ...commonOptions } ) }) -describe.skip('weakMapMemoize simple examples', () => { +describe('weakMapMemoize simple examples', () => { const selectorDefault = createSelector( [(state: RootState) => state.todos], todos => todos.map(({ id }) => id) @@ -367,16 +367,13 @@ describe.skip('weakMapMemoize simple examples', () => { selectorDefault(store.getState()) }, { - ...options, + ...commonOptions, setup: (task, mode) => { if (mode === 'warmup') return selectorDefault.clearCache() selectorDefault.resetRecomputations() selectorDefault.memoizedResultFunc.clearCache() task.opts = { - // beforeEach: () => { - // store.dispatch(toggleCompleted(0)) - // }, afterAll: () => { console.log( `${selectorDefault.name} recomputations after:`, @@ -393,16 +390,13 @@ describe.skip('weakMapMemoize simple examples', () => { selectorWeakMap(store.getState()) }, { - ...options, + ...commonOptions, setup: (task, mode) => { if (mode === 'warmup') return selectorWeakMap.clearCache() selectorWeakMap.resetRecomputations() selectorWeakMap.memoizedResultFunc.clearCache() task.opts = { - // beforeEach: () => { - // store.dispatch(toggleCompleted(0)) - // }, afterAll: () => { console.log( `${selectorWeakMap.name} recomputations after:`, @@ -419,16 +413,13 @@ describe.skip('weakMapMemoize simple examples', () => { selectorAutotrack(store.getState()) }, { - ...options, + ...commonOptions, setup: (task, mode) => { if (mode === 'warmup') return selectorAutotrack.clearCache() selectorAutotrack.resetRecomputations() selectorAutotrack.memoizedResultFunc.clearCache() task.opts = { - // beforeEach: () => { - // store.dispatch(toggleCompleted(0)) - // }, afterAll: () => { console.log( `${selectorAutotrack.name} recomputations after:`, diff --git a/test/examples.test.ts b/test/examples.test.ts new file mode 100644 index 000000000..973435521 --- /dev/null +++ b/test/examples.test.ts @@ -0,0 +1,161 @@ +import type { + OutputSelector, + Selector, + SelectorArray, + UnknownMemoizer +} from 'reselect' +import { + createSelector, + createSelectorCreator, + defaultMemoize, + unstable_autotrackMemoize as autotrackMemoize, + weakMapMemoize +} from 'reselect' +import { test } from 'vitest' +import type { RootState } from './testUtils' +import { addTodo, setupStore } from './testUtils' + +const store = setupStore() + +const EMPTY_ARRAY: [] = [] + +export const fallbackToEmptyArray = (array: T[]) => { + return array.length === 0 ? EMPTY_ARRAY : array +} + +const selectCompletedTodos = createSelector( + [(state: RootState) => state.todos], + todos => { + return fallbackToEmptyArray(todos.filter(todo => todo.completed === true)) + } +) + +const completedTodos = selectCompletedTodos(store.getState()) + +store.dispatch(addTodo({ title: '', description: '' })) + +test('empty array', () => { + expect(completedTodos).toBe(selectCompletedTodos(store.getState())) +}) + +test('identity', () => { + const identity = any>(func: Func) => func + const createNonMemoizedSelector = createSelectorCreator({ + memoize: identity, + argsMemoize: identity + }) + const nonMemoizedSelector = createNonMemoizedSelector( + [(state: RootState) => state.todos], + todos => todos.filter(todo => todo.completed === true) + ) + + nonMemoizedSelector(store.getState()) + nonMemoizedSelector(store.getState()) + nonMemoizedSelector(store.getState()) + + expect(nonMemoizedSelector.recomputations()).toBe(3) +}) + +test('Top Level Selectors', () => { + type TopLevelSelectors = { + [K in keyof State as K extends string + ? `select${Capitalize}` + : never]: Selector + } + + const topLevelSelectors: TopLevelSelectors = { + selectAlerts: state => state.alerts, + selectTodos: state => state.todos, + selectUsers: state => state.users + } +}) + +test('Find Fastest Selector', () => { + const store = setupStore() + const selectTodoIds = createSelector( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id) + ) + const findFastestSelector = ( + selector: S, + ...selectorArgs: Parameters + ) => { + const memoizeFuncs = [defaultMemoize, weakMapMemoize, autotrackMemoize] + const results = memoizeFuncs + .map(memoize => { + const alternateSelector = createSelector( + selector.dependencies as [...SelectorArray], + selector.resultFunc, + { memoize } + ) + const start = performance.now() + alternateSelector.apply(null, selectorArgs) + const time = performance.now() - start + return { name: memoize.name, time, selector: alternateSelector } + }) + .sort((a, b) => a.time - b.time) + const fastest = results.reduce((minResult, currentResult) => + currentResult.time < minResult.time ? currentResult : minResult + ) + const ratios = results + .filter(({ time }) => time !== fastest.time) + .map( + ({ time, name }) => + `\x1B[33m \x1B[1m${ + time / fastest.time + }\x1B[0m times faster than \x1B[1;41m${name}\x1B[0m.` + ) + if (fastest.selector.memoize.name !== selector.memoize.name) { + console.warn( + `The memoization method for \x1B[1;41m${ + selector.name + }\x1B[0m is \x1B[31m${ + selector.memoize.name + }\x1B[0m!\nChange it to \x1B[32m\x1B[1m${ + fastest.selector.memoize.name + }\x1B[0m to be more efficient.\nYou should use \x1B[32m\x1B[1m${ + fastest.name + }\x1B[0m because it is${ratios.join('\nand\n')}` + ) + } + return { results, fastest } as const + } + + expect( + findFastestSelector(selectTodoIds, store.getState()).fastest.name + ).toBe(weakMapMemoize.name) +}) + +test('TypedCreateSelector', () => { + type TypedCreateSelector< + State, + MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize + > = < + InputSelectors extends readonly Selector[], + Result, + OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction, + OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction + >( + ...createSelectorArgs: Parameters< + typeof createSelector< + InputSelectors, + Result, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > + > + ) => ReturnType< + typeof createSelector< + InputSelectors, + Result, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > + > + const createAppSelector: TypedCreateSelector = createSelector + const selector = createAppSelector( + [state => state.todos, (state, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id)?.completed + ) +}) From bb82bf31f7704fbb74e87b264802711859b978d9 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Mon, 13 Nov 2023 11:01:15 -0600 Subject: [PATCH 17/50] Add `createCurriedSelectorCreator` to docs. --- README.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/README.md b/README.md index b78a73510..5fffbfb9e 100644 --- a/README.md +++ b/README.md @@ -1656,6 +1656,44 @@ const MyComponent: FC = ({ id }) => { } ``` +We're also exporting a `createCurriedSelectorCreator` function for ease of use: + +```ts +import { + createCurriedSelectorCreator, + createSelector, + weakMapMemoize +} from 'reselect' + +const parametricSelector = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.filter(todo => todo.id === id), + { memoize: weakMapMemoize, argsMemoize: weakMapMemoize } +) + +const createCurriedSelectorWeakMap = createCurriedSelectorCreator({ + memoize: weakMapMemoize, + argsMemoize: weakMapMemoize +}) + +const curriedSelector = createCurriedSelectorWeakMap( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.filter(todo => todo.id === id) +) + +// This: +parametricSelector(state, 0) + +// Is the same as this: +curriedSelector(0)(state) + +// Inside your component you can replace this: +const selectTodo = useSelector(state => parametricSelector(state, id)) + +// With this: +const selectTodo = useSelector(curriedSelector(id)) +``` +
### How can I make pre-typed version of [`createSelector`](#createselectorinputselectors--inputselectors-resultfunc-createselectoroptions) for my root state? From 3b793a314114f0851c90640c29730f4d2c99853c Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 15 Nov 2023 01:52:21 -0600 Subject: [PATCH 18/50] Fix library names formatting in docs --- README.md | 219 +++++++++++++++++++++++++++--------------------------- 1 file changed, 110 insertions(+), 109 deletions(-) diff --git a/README.md b/README.md index 5fffbfb9e..9b777288b 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,17 @@ A library for creating memoized "selector" functions. Commonly used with Redux, but usable with any plain JS immutable data as well. -- Selectors can compute derived data, allowing `Redux` to store the minimal possible state. +- Selectors can compute derived data, allowing [Redux] to store the minimal possible state. - Selectors are efficient. A selector is not recomputed unless one of its arguments changes. - Selectors are composable. They can be used as input to other selectors. -The **Redux docs usage page on [Deriving Data with Selectors](https://redux.js.org/usage/deriving-data-selectors)** covers the purpose and motivation for selectors, why memoized selectors are useful, typical `Reselect` usage patterns, and using selectors with `React-Redux`. +The **Redux docs usage page on [Deriving Data with Selectors](https://redux.js.org/usage/deriving-data-selectors)** covers the purpose and motivation for selectors, why memoized selectors are useful, typical Reselect usage patterns, and using selectors with [React-Redux]. ## Installation ### Redux Toolkit -While `Reselect` is not exclusive to `Redux`, it is already included by default in [the official Redux Toolkit package](https://redux-toolkit.js.org) - no further installation needed. +While Reselect is not exclusive to [Redux], it is already included by default in [the official Redux Toolkit package](https://redux-toolkit.js.org) - no further installation needed. ```ts import { createSelector } from '@reduxjs/toolkit' @@ -22,7 +22,7 @@ import { createSelector } from '@reduxjs/toolkit' ### Standalone -For standalone usage, install the `Reselect` package: +For standalone usage, install the `reselect` package: #### Using `npm` @@ -52,7 +52,7 @@ pnpm add reselect ## Basic Usage -`Reselect` exports a [`createSelector`] API, which generates memoized selector functions. [`createSelector`] accepts one or more [`input selectors`], which extract values from arguments, and a [`combiner`] function that receives the extracted values and should return a derived value. If the generated [`output selector`] is called multiple times, the output will only be recalculated when the extracted values have changed. +Reselect exports a [`createSelector`] API, which generates memoized selector functions. [`createSelector`] accepts one or more [input selectors], which extract values from arguments, and a [result function] function that receives the extracted values and should return a derived value. If the generated [output selector] is called multiple times, the output will only be recalculated when the extracted values have changed. You can play around with the following **example** in [this CodeSandbox](https://codesandbox.io/s/reselect-example-g3k9gf?file=/src/index.js): @@ -105,7 +105,7 @@ console.log( As you can see from the example above, `memoizedSelectCompletedTodos` does not run the second or third time, but we still get the same return value as last time. -Another difference is that with `memoizedSelectCompletedTodos` the referential integrity of the return value is also maintained through multiple calls of the selector, but the same cannot be said about the first example. +In addition to skipping unnecessary recalculations, `memoizedSelectCompletedTodos` returns the existing result reference if there is no recalculation. This is important for libraries like [React-Redux] or [React] that often rely on reference equality checks to optimize UI updates. --- @@ -116,10 +116,10 @@ Another difference is that with `memoizedSelectCompletedTodos` the referential i - [Standalone](#standalone) - [Basic Usage](#basic-usage) - [Terminology](#terminology) -- [How Does `Reselect` Work?](#how-does-reselect-work) +- [How Does Reselect Work?](#how-does-reselect-work) - [Cascading Memoization](#cascading-memoization) - - [`Reselect` Vs Standard Memoization](#reselect-vs-standard-memoization) - - [Why `Reselect` Is Often Used With `Redux`](#why-reselect-is-often-used-with-redux) + - [Reselect Vs Standard Memoization](#reselect-vs-standard-memoization) + - [Why Reselect Is Often Used With [Redux]](#why-reselect-is-often-used-with-redux) - [API](#api) - [**`createSelector`**][`createSelector`] - [**`defaultMemoize`**][`defaultMemoize`] @@ -130,11 +130,11 @@ Another difference is that with `memoizedSelectCompletedTodos` the referential i - [**`createCurriedSelector`**][`createCurriedSelector`] - [Debugging Tools](#debuggingtools) - [What's New in 5.0.0?](#v5summary) -- [Optimizing `Reselect`](#optimizing-reselect) +- [Optimizing Reselect](#optimizing-reselect) - [FAQ](#faq) - [Why isn’t my selector recomputing when the input state changes?](#why-isnt-my-selector-recomputing-when-the-input-state-changes) - [Why is my selector recomputing when the input state stays the same?](#why-is-my-selector-recomputing-when-the-input-state-stays-the-same) - - [Can I use `Reselect` without `Redux`?](#can-i-use-reselect-without-redux) + - [Can I use Reselect without [Redux]?](#can-i-use-reselect-without-redux) - [How do I create a selector that takes an argument?](#how-do-i-create-a-selector-that-takes-an-argument) - [The default memoization function is no good, can I use a different one?](#the-default-memoization-function-is-no-good-can-i-use-a-different-one) - [How do I test a selector?](#how-do-i-test-a-selector) @@ -155,19 +155,18 @@ Another difference is that with `memoizedSelectCompletedTodos` the referential i
Click to expand -- [**`Selector Function`**](#selector-function): A function that takes the `Redux` store state (or a part of it) as an argument and returns data derived from that state. -- [**`input selectors`**](#input-selectors): Basic selector functions used as building blocks for creating a memoized selector. They are passed as the first argument(s) to [`createSelector`]. -- [**`Output Selector`**](#output-selector): The actual memoized selectors created by [`createSelector`]. -- [**`Result Function (Combiner)`**](#result-function): The function that comes after the [`input selectors`]. It takes the [`input selectors`]' return values as arguments and returns a result. Otherwise known as the `combiner`. -- [**`Combiner`**](#combiner): A function that takes [`input selectors`]' return values as arguments and returns a result (This function comes after the [`input selectors`] inside [`createSelector`]). This term is somewhat interchangeably used with `Result Function` or `resultFunc`. But `combiner` is more of a general term and `resultFunc` is more specific to `Reselect`. So the `resultFunc` is a `combiner` but a `combiner` is not necessarily the same as `resultFunc`. For the sake of simplicity, they are used synonymously throughout this documentation. -- [**`Dependencies`**](#dependencies): Same as [`input selectors`]. They are what the [`output selector`] "depends" on. +- [**Selector Function**](#selector-function): A function that accepts one or more JavaScript values as arguments, and derives a result. When used with [Redux], the first argument is typically the entire Redux store state. +- [**input selectors**](#input-selectors): Basic selector functions used as building blocks for creating a memoized selector. They are passed as the first argument(s) to [`createSelector`], and are called with all selector arguments. They are responsible for extracting and providing necessary values to the [result function]. +- [**Output Selector**](#output-selector): The actual memoized selectors created by [`createSelector`]. +- [**Result Function**](#result-function): The function that comes after the [input selectors]. It takes the [input selectors]' return values as arguments and returns a result. +- [**`Dependencies`**](#dependencies): Same as [input selectors]. They are what the [output selector] "depends" on. The below example serves as a visual aid: ```ts const outputSelector = createSelector( [inputSelector1, inputSelector2, inputSelector3], // synonymous with `dependencies`. - combiner // synonymous with `Result Function` or `resultFunc`. + resultFunc // Result function ) ``` @@ -177,7 +176,7 @@ const outputSelector = createSelector( --- -## How Does `Reselect` Work? +## How Does Reselect Work? @@ -185,29 +184,29 @@ const outputSelector = createSelector(
Click to expand -The way `Reselect` works can be broken down into multiple parts: +The way Reselect works can be broken down into multiple parts: -1. **Initial Run**: On the first call, `Reselect` runs all the [`input selectors`], gathers their results, and passes them to the [`result function`]. +1. **Initial Run**: On the first call, Reselect runs all the [input selectors], gathers their results, and passes them to the [result function]. -2. **Subsequent Runs**: For subsequent calls, `Reselect` performs two levels of checks: +2. **Subsequent Runs**: For subsequent calls, Reselect performs two levels of checks: - **First Level**: It compares the current arguments with the previous ones (done by `argsMemoize`). - - If they're the same, it returns the cached result without running the [`input selectors`] or the [`result function`]. + - If they're the same, it returns the cached result without running the [input selectors] or the [result function]. - If they differ, it proceeds to the second level. - - **Second Level**: It runs the [`input selectors`] and compares their current results with the previous ones (done by `memoize`). + - **Second Level**: It runs the [input selectors] and compares their current results with the previous ones (done by `memoize`). > [!NOTE] - > If any one of the [`input selectors`] return a different result, all [`input selectors`] will recalculate. - - If the results are the same, it returns the cached result without running the [`result function`]. - - If the results differ, it runs the [`result function`]. + > If any one of the [input selectors] return a different result, all [input selectors] will recalculate. + - If the results are the same, it returns the cached result without running the [result function]. + - If the results differ, it runs the [result function]. This behavior is what we call **_Cascading Double-Layer Memoization_**.
-### `Reselect` Vs Standard Memoization +### Reselect Vs Standard Memoization
Click to expand @@ -217,20 +216,20 @@ This behavior is what we call **_Cascading Double-Layer Memoization_**. _Standard memoization only compares arguments. If they're the same, it returns the cached result._ -##### Memoization with `Reselect` +##### Memoization with Reselect ![reselect-memoization](docs/assets//reselect-memoization.png) -_`Reselect` adds a second layer of checks with the [`input selectors`]. This is crucial in `Redux` applications where state references change frequently._ +_Reselect adds a second layer of checks with the [input selectors]. This is crucial in [Redux] applications where state references change frequently._ -A normal [memoization] function will compare the arguments, and if they are the same as last time, it will skip running the function and return the cached result. However, `Reselect` enhances this by introducing a second tier of checks via its [`input selectors`]. It's possible that the arguments passed to these [`input selectors`] may change, yet their results remain the same. When this occurs, `Reselect` avoids re-executing the [`result function`], and returns the cached result. +A normal [memoization] function will compare the arguments, and if they are the same as last time, it will skip running the function and return the cached result. However, Reselect enhances this by introducing a second tier of checks via its [input selectors]. It's possible that the arguments passed to these [input selectors] may change, yet their results remain the same. When this occurs, Reselect avoids re-executing the [result function], and returns the cached result. -This feature becomes crucial in `Redux` applications, where the `state` changes its reference anytime an `action` is dispatched. +This feature becomes crucial in [Redux] applications, where the `state` changes its reference anytime an `action` is dispatched. > [!NOTE] -> The [`input selectors`] take the same arguments as the [`output selector`]. +> The [input selectors] take the same arguments as the [output selector]. -### Why `Reselect` Is Often Used With `Redux` +### Why Reselect Is Often Used With [Redux] Imagine you have a selector like this: @@ -262,9 +261,9 @@ store.dispatch(toggleRead(0)) selectCompletedTodos(store.getState()) // It recalculates. ``` -But why? `selectCompletedTodos` only needs to access `state.todos`, and has nothing to do with `state.alerts`, so why have we broken memoization? Well that's because in `Redux` anytime you make a change to the root `state`, it gets shallowly updated, which means its reference changes, therefore a normal memoization function will always fail the comparison check on the arguments. +But why? `selectCompletedTodos` only needs to access `state.todos`, and has nothing to do with `state.alerts`, so why have we broken memoization? Well that's because in [Redux] anytime you make a change to the root `state`, it gets shallowly updated, which means its reference changes, therefore a normal memoization function will always fail the comparison check on the arguments. -But with `Reselect`, we can do something like this: +But with Reselect, we can do something like this: ```ts const selectCompletedTodos = createSelector( @@ -282,7 +281,7 @@ store.dispatch(toggleRead(0)) selectCompletedTodos(store.getState()) // The `input selectors` will run, But the `result function` is skipped and the cached result will be returned. ``` -Even when the overall `state` changes, `Reselect` ensures efficient memoization through its unique approach. The [`result function`] doesn't re-run if the relevant part of the `state` (in this case `state.todos`), remains unchanged. This is due to `Reselect`'s [**_Cascading Double-Layer Memoization_**][**_Cascading Memoization_**]. The first layer checks the entire `state`, and the second layer checks the results of the [`input selectors`]. If the first layer fails (due to a change in the overall `state`) but the second layer succeeds (because `state.todos` is unchanged), `Reselect` skips recalculating the [`result function`]. This dual-check mechanism makes `Reselect` particularly effective in `Redux` applications, ensuring computations are only done when truly necessary. +Even when the overall `state` changes, Reselect ensures efficient memoization through its unique approach. The [result function] doesn't re-run if the relevant part of the `state` (in this case `state.todos`), remains unchanged. This is due to Reselect's [**_Cascading Double-Layer Memoization_**][**_Cascading Memoization_**]. The first layer checks the entire `state`, and the second layer checks the results of the [input selectors]. If the first layer fails (due to a change in the overall `state`) but the second layer succeeds (because `state.todos` is unchanged), Reselect skips recalculating the [result function]. This dual-check mechanism makes Reselect particularly effective in [Redux] applications, ensuring computations are only done when truly necessary.
@@ -298,25 +297,25 @@ Even when the overall `state` changes, `Reselect` ensures efficient memoization
Description -Accepts one or more ["input selectors"][`input selectors`] (either as separate arguments or a single array), -a single ["result function"][`result function`] / ["combiner"][`combiner`], and an optional options object, and +Accepts one or more "[input selectors]" (either as separate arguments or a single array), +a single "[result function]", and an optional options object, and generates a memoized selector function.
Parameters -| Name | Description | -| :----------------------- | :----------------------------------------------------------------------------------------------- | -| `inputSelectors` | An array of [`input selectors`], can also be passed as separate arguments. | -| `combiner` | A [`combiner`] function that takes the results of the [`input selectors`] as separate arguments. | -| `createSelectorOptions?` | An optional options object that allows for further customization per selector. | +| Name | Description | +| :----------------------- | :-------------------------------------------------------------------------------- | +| `inputSelectors` | An array of [input selectors], can also be passed as separate arguments. | +| `resultFunc` | A function that takes the results of the [input selectors] as separate arguments. | +| `createSelectorOptions?` | An optional options object that allows for further customization per selector. |
Returns -A memoized [`output selector`]. +A memoized [output selector].
@@ -324,8 +323,8 @@ A memoized [`output selector`]. | Name | Description | | :---------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `InputSelectors` | The type of the [`input selectors`] array. | -| `Result` | The return type of the [`combiner`] as well as the [`output selector`]. | +| `InputSelectors` | The type of the [input selectors] array. | +| `Result` | The return type of the [result function] as well as the [output selector]. | | `OverrideMemoizeFunction` | The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into [`createSelectorCreator`]. | | `OverrideArgsMemoizeFunction` | The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into [`createSelectorCreator`]. | @@ -789,7 +788,7 @@ Accepts either a `memoize` function and `...memoizeOptions` rest parameter, or * | Name | Description | | :----------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `options` | An options object containing the `memoize` function responsible for memoizing the `resultFunc` inside [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). It also provides additional options for customizing memoization. While the `memoize` property is mandatory, the rest are optional. | -| `options.argsMemoize?` | The optional memoize function that is used to memoize the arguments passed into the `output selector` generated by [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`).
**`Default`** `defaultMemoize` | +| `options.argsMemoize?` | The optional memoize function that is used to memoize the arguments passed into the [output selector] generated by [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`).
**`Default`** `defaultMemoize` | | `options.argsMemoizeOptions?` | Optional configuration options for the `argsMemoize` function. These options are passed to the `argsMemoize` function as the second argument.
**`Since`** 5.0.0 | | `options.inputStabilityCheck?` | Overrides the global input stability check for the selector. Possible values are:
`once` - Run only the first time the selector is called.
`always` - Run every time the selector is called.
`never` - Never run the input stability check.
**`Default`** = `'once'`
**`Since`** 5.0.0 | | `options.memoize` | The memoize function that is used to memoize the `resultFunc` inside [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). **`Since`** 5.0.0 | @@ -814,10 +813,10 @@ A customized [`createSelector`] function.
Type parameters -| Name | Description | -| :-------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `MemoizeFunction` | The type of the memoize function that is used to memoize the `resultFunc` inside [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). | -| `ArgsMemoizeFunction` | The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used. | +| Name | Description | +| :-------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `MemoizeFunction` | The type of the memoize function that is used to memoize the `resultFunc` inside [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). | +| `ArgsMemoizeFunction` | The type of the optional memoize function that is used to memoize the arguments passed into the [output selector] generated by [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used. |
@@ -922,8 +921,8 @@ const selector = customSelectorCreator(
Description -A convenience function for a common pattern that arises when using `Reselect`. -The selector passed to a `connect` decorator often just takes the values of its [`input selectors`] +A convenience function for a common pattern that arises when using Reselect. +The selector passed to a `connect` decorator often just takes the values of its [input selectors] and maps them to keys in an object.
@@ -947,7 +946,7 @@ A memoized structured selector. | Name | Description | | :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `InputSelectorsObject` | The shape of the [`input selectors`] object. | +| `InputSelectorsObject` | The shape of the [input selectors] object. | | `MemoizeFunction` | The type of the memoize function that is used to create the structured selector. It defaults to `defaultMemoize`. | | `ArgsMemoizeFunction` | The type of the of the memoize function that is used to memoize the arguments passed into the generated structured selector. It defaults to `defaultMemoize`. | @@ -1037,7 +1036,7 @@ const result = structuredSelector({ a: 1, b: 2 }) // will produce { x: 1, y: 2 } #### `inputStabilityCheck` -Due to how [`Cascading Memoization`][**_Cascading Memoization_**] works in `Reselect`, it is crucial that your [`input selectors`] do not return a new reference on each run. If an [`input selector`][`input selectors`] always returns a new reference, like +Due to how [**_Cascading Memoization_**] works in Reselect, it is crucial that your [input selectors] do not return a new reference on each run. If an [input selector][input selectors] always returns a new reference, like ```ts state => ({ a: state.a, b: state.b }) @@ -1050,7 +1049,7 @@ state => state.todos.map(todo => todo.id) ``` that will cause the selector to never memoize properly. -Since this is a common mistake, we've added a development mode check to catch this. By default, [`createSelector`] will now run the [`input selectors`] twice during the first call to the selector. If the result appears to be different for the same call, it will log a warning with the arguments and the two different sets of extracted input values. +Since this is a common mistake, we've added a development mode check to catch this. By default, [`createSelector`] will now run the [input selectors] twice during the first call to the selector. If the result appears to be different for the same call, it will log a warning with the arguments and the two different sets of extracted input values. ```ts type StabilityCheckFrequency = 'always' | 'once' | 'never' @@ -1071,7 +1070,7 @@ You can configure this behavior in two ways: ##### 1. Globally through `setInputStabilityCheckEnabled`: -A `setInputStabilityCheckEnabled` function is exported from `Reselect`, which should be called with the desired setting. +A `setInputStabilityCheckEnabled` function is exported from Reselect, which should be called with the desired setting. ```ts import { setInputStabilityCheckEnabled } from 'reselect' @@ -1111,20 +1110,20 @@ const selectCompletedTodosLength = createSelector( ### Output Selector Fields -
The output selectors created by createSelector have several additional properties attached to them: - -| Name | Description | -| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `resultFunc` | The final function passed to [`createSelector`]. Otherwise known as the `combiner`. | -| `memoizedResultFunc` | The memoized version of `resultFunc`. | -| `lastResult` | Returns The last result calculated by `memoizedResultFunc`. | -| `dependencies` | The array of the input selectors used by [`createSelector`] to compose the combiner (`memoizedResultFunc`). | -| `recomputations` | Counts the number of times `memoizedResultFunc` has been recalculated. | -| `resetRecomputations` | Resets the count of `recomputations` count to 0. | -| `dependencyRecomputations` | Counts the number of times the [`input selectors`] ([`dependencies`]) have been recalculated. This is distinct from `recomputations`, which tracks the recalculations of the [`result function`]. | -| `resetDependencyRecomputations` | Resets the `dependencyRecomputations` count to 0. | -| `memoize` | Function used to memoize the `resultFunc`. | -| `argsMemoize` | Function used to memoize the arguments passed into the [`output selector`]. | +
The output selectors created by createSelector have several additional properties attached to them: + +| Name | Description | +| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `resultFunc` | The final function passed to [`createSelector`]. | +| `memoizedResultFunc` | The memoized version of `resultFunc`. | +| `lastResult` | Returns The last result calculated by `memoizedResultFunc`. | +| `dependencies` | The array of the input selectors used by [`createSelector`] to compose `resultFunc`. | +| `recomputations` | Counts the number of times `memoizedResultFunc` has been recalculated. | +| `resetRecomputations` | Resets the count of `recomputations` count to 0. | +| `dependencyRecomputations` | Counts the number of times the [input selectors] ([`dependencies`]) have been recalculated. This is distinct from `recomputations`, which tracks the recalculations of the [result function]. | +| `resetDependencyRecomputations` | Resets the `dependencyRecomputations` count to 0. | +| `memoize` | Function used to memoize the `resultFunc`. | +| `argsMemoize` | Function used to memoize the arguments passed into the [output selector]. |
@@ -1146,12 +1145,12 @@ const selectCompletedTodosLength = createSelector( - **Memoization Functions**: - Introduced new experimental memoization functions: `weakMapMemoize` and `autotrackMemoize`. - - Incorporated `memoize` and `argsMemoize` into the [`output selector fields`] for debugging purposes. + - Incorporated `memoize` and `argsMemoize` into the [output selector fields] for debugging purposes. - **TypeScript Support and Performance**: - - Discontinued support for `TypeScript` versions below 4.7, aligning with modern `TypeScript` features. - - Significantly improved `TypeScript` performance for nesting `output selectors`. The nesting limit has increased from approximately 8 to around 30 `output selectors`, greatly reducing the occurrence of the infamous `Type instantiation is excessively deep and possibly infinite` error. + - Discontinued support for TypeScript versions below 4.7, aligning with modern TypeScript features. + - Significantly improved TypeScript performance for nesting [output selector]s. The nesting limit has increased from approximately 8 to around 30 [output selector]s, greatly reducing the occurrence of the infamous `Type instantiation is excessively deep and possibly infinite` error. - **Selector API Enhancements**: @@ -1161,8 +1160,8 @@ const selectCompletedTodosLength = createSelector( - **Additional Functionalities**: - - Added `dependencyRecomputations` and `resetDependencyRecomputations` to the [`output selector fields`]. These additions provide greater control and insight over [`input selectors`], complementing the new `argsMemoize` API. - - Introduced `inputStabilityCheck`, a development tool that runs the [`input selectors`] twice using the same arguments and triggers a warning If they return differing results for the same call. + - Added `dependencyRecomputations` and `resetDependencyRecomputations` to the [output selector fields]. These additions provide greater control and insight over [input selectors], complementing the new `argsMemoize` API. + - Introduced `inputStabilityCheck`, a development tool that runs the [input selectors] twice using the same arguments and triggers a warning If they return differing results for the same call. These updates aim to enhance flexibility, performance, and developer experience. For detailed usage and examples, refer to the updated documentation sections for each feature. @@ -1174,13 +1173,13 @@ These updates aim to enhance flexibility, performance, and developer experience. -## Optimizing `Reselect` +## Optimizing Reselect ### Common Mistakes
Click to expand -A somewhat common mistake is to write an [`input selector`][`input selectors`] that extracts a value or does some derivation, and a [`result function`] that just returns its result: +A somewhat common mistake is to write an [input selector][input selectors] that extracts a value or does some derivation, and a [result function] that just returns its result: ```ts // ❌ BROKEN: this will not memoize correctly, and does nothing useful! @@ -1190,7 +1189,7 @@ const brokenSelector = createSelector( ) ``` -Any [`result function`] that just returns its inputs is incorrect! The [`result function`] should always have the transformation logic. +Any [result function] that just returns its inputs is incorrect! The [result function] should always have the transformation logic. Similarly: @@ -1250,7 +1249,7 @@ const selectCompletedTodos = createSelector( ) ``` -This way if the [`result function`] returns an empty array twice in a row, your component will not re-render due to a stable empty array reference: +This way if the [result function] returns an empty array twice in a row, your component will not re-render due to a stable empty array reference: ```ts const completedTodos = selectCompletedTodos(store.getState()) @@ -1266,12 +1265,12 @@ console.log(completedTodos === selectCompletedTodos(store.getState())) //=> true
Click to expand -There are a few details that will help you skip running as many functions as possible and get the best possible performance out of `Reselect`: +There are a few details that will help you skip running as many functions as possible and get the best possible performance out of Reselect: -- Due to the [`Cascading Memoization`][**_Cascading Memoization_**] in `Reselect`, The first layer of checks is upon the arguments that are passed to the [`output selector`], therefore it's best to maintain the same reference for the arguments as much as possible. -- In `Redux`, your state will change reference when updated. But it's best to keep the additional arguments as simple as possible, you can pass in objects or array as long as their reference does not change. Or you can pass in primitives like numbers for ids. -- Keep your [`input selectors`] as simple as possible. It's best if they mostly consist of field accessors like `state => state.todos` or argument providers like `(state, id) => id`. You should not be doing any sort of calculation inside [`input selectors`], and you should definitely not be returning an object or array with a new reference each time. -- The [`result function`] is only re-run as a last resort. So make sure to put any and all calculations inside your [`result function`]. That way, `Reselect` will only run those calculations if all other checks fail. +- Due to the [**_Cascading Memoization_**] in Reselect, The first layer of checks is upon the arguments that are passed to the [output selector], therefore it's best to maintain the same reference for the arguments as much as possible. +- In [Redux], your state will change reference when updated. But it's best to keep the additional arguments as simple as possible, you can pass in objects or array as long as their reference does not change. Or you can pass in primitives like numbers for ids. +- Keep your [input selectors] as simple as possible. It's best if they mostly consist of field accessors like `state => state.todos` or argument providers like `(state, id) => id`. You should not be doing any sort of calculation inside [input selectors], and you should definitely not be returning an object or array with a new reference each time. +- The [result function] is only re-run as a last resort. So make sure to put any and all calculations inside your [result function]. That way, Reselect will only run those calculations if all other checks fail. This: @@ -1346,7 +1345,7 @@ const selectCompletedTodos = createSelector(
Answer -Check that your memoization function is compatible with your state update function (i.e. the reducer if you are using `Redux`). For example, a selector created with [`createSelector`] will not work with a state update function that mutates an existing object instead of creating a new one each time. [`createSelector`] uses an identity check (`===`) to detect that an input has changed, so mutating an existing object will not trigger the selector to recompute because mutating an object does not change its identity. Note that if you are using `Redux`, mutating the state object is [almost certainly a mistake](http://redux.js.org/docs/Troubleshooting.html). +Check that your memoization function is compatible with your state update function (i.e. the reducer if you are using [Redux]). For example, a selector created with [`createSelector`] will not work with a state update function that mutates an existing object instead of creating a new one each time. [`createSelector`] uses an identity check (`===`) to detect that an input has changed, so mutating an existing object will not trigger the selector to recompute because mutating an object does not change its identity. Note that if you are using [Redux], mutating the state object is [almost certainly a mistake](http://redux.js.org/docs/Troubleshooting.html).
@@ -1354,9 +1353,9 @@ Check that your memoization function is compatible with your state update functi
Answer -To address unexpected recomputations in your selector, first ensure that `inputStabilityCheck` is set to either `'always'` or `'once'`. This setting aids in debugging by monitoring the stability of your inputs. Additionally, utilize [`output selector fields`] such as `recomputations`, `resetRecomputations`, `dependencyRecomputations`, and `resetDependencyRecomputations`. These tools help identify the source of the issue. +To address unexpected recomputations in your selector, first ensure that `inputStabilityCheck` is set to either `'always'` or `'once'`. This setting aids in debugging by monitoring the stability of your inputs. Additionally, utilize [output selector fields] such as `recomputations`, `resetRecomputations`, `dependencyRecomputations`, and `resetDependencyRecomputations`. These tools help identify the source of the issue. -Keep an eye on the `dependencyRecomputations` count. If it increases while `recomputations` remains the same, it suggests that your arguments are changing references but your [`input selectors`] are stable which is typically the desired behavior. +Keep an eye on the `dependencyRecomputations` count. If it increases while `recomputations` remains the same, it suggests that your arguments are changing references but your [input selectors] are stable which is typically the desired behavior. To delve deeper, you can determine which arguments are changing references too frequently by using the `argsMemoizeOptions` and `equalityCheck`. Consider the following example: @@ -1374,7 +1373,7 @@ const selectAlertsByType = createSelector( (alerts, type) => alerts.filter(todo => todo.type === type), { argsMemoizeOptions: { - // This will check the arguments passed to the `output selector`. + // This will check the arguments passed to the output selector. equalityCheck: (a, b) => { if (a !== b) { console.log('Changed argument:', a, 'to', b) @@ -1388,11 +1387,11 @@ const selectAlertsByType = createSelector(
-### Can I use `Reselect` without `Redux`? +### Can I use Reselect without [Redux]?
Answer -Yes. `Reselect` has no dependencies on any other package, so although it was designed to be used with `Redux` it can be used independently. It can be used with any plain JS data, such as typical `React` state values, as long as that data is being updated immutably. +Yes. Reselect has no dependencies on any other package, so although it was designed to be used with [Redux] it can be used independently. It can be used with any plain JS data, such as typical [React] state values, as long as that data is being updated immutably.
@@ -1400,11 +1399,11 @@ Yes. `Reselect` has no dependencies on any other package, so although it was des
Answer -When creating a selector that accepts arguments in `Reselect`, it's important to structure your input and [`output selectors`][Output Selectors] appropriately. Here are key points to consider: +When creating a selector that accepts arguments in Reselect, it's important to structure your input and [output selector]s appropriately. Here are key points to consider: -1. **Consistency in Arguments**: Ensure that all positional arguments across [`input selectors`] are of the same type for consistency. +1. **Consistency in Arguments**: Ensure that all positional arguments across [input selectors] are of the same type for consistency. -2. **Selective Argument Usage**: Design each selector to use only its relevant argument(s) and ignore the rest. This is crucial because all [`input selectors`] receive the same arguments that are passed to the [`output selector`][Output Selectors]. +2. **Selective Argument Usage**: Design each selector to use only its relevant argument(s) and ignore the rest. This is crucial because all [input selectors] receive the same arguments that are passed to the [output selector]. Suppose we have the following state structure: @@ -1437,7 +1436,7 @@ const selectAvailableItems = createSelector( ) ``` -Internally `Reselect` is doing this: +Internally Reselect is doing this: ```ts // Input selector #1 @@ -1446,9 +1445,9 @@ const items = (state: RootState, category: string, id: number) => state.items const category = (state: RootState, category: string, id: number) => category // Input selector #3 const id = (state: RootState, category: string, id: number) => id -// result of `output selector` +// result of output selector const finalResult = - // The `Result Function` + // The result function items.filter(item => item.category === category && item.id !== id) ``` @@ -1566,7 +1565,7 @@ Yes, as of 5.0.0 you can use [`weakMapMemoize`](#weakmapmemoizefunc---since-500)
Answer -Yes! `Reselect` is now written in `TypeScript` itself, so they should Just Work™. +Yes! Reselect is now written in TypeScript itself, so they should Just Work™.
@@ -1624,7 +1623,7 @@ Now you can do this: const selectTodo = useSelector(curriedSelector(id)) ``` -Another thing you can do if you are using `React-Redux` is create a custom hook factory function: +Another thing you can do if you are using [React-Redux] is create a custom hook factory function: ```ts import type { GetParamsFromSelectors, Selector } from 'reselect' @@ -1746,7 +1745,7 @@ export type TypedCreateSelector< export const createAppSelector: TypedCreateSelector = createSelector ``` -> [!WARNING]: This approach currently only supports [`input selectors`] provided as a single array. +> [!WARNING]: This approach currently only supports [input selectors] provided as a single array.
@@ -1788,7 +1787,7 @@ const createNonMemoizedSelector = createSelectorCreator({ ### [re-reselect](https://github.com/toomuchdesign/re-reselect) -Enhances `Reselect` selectors by wrapping [`createSelector`] and returning a memoized collection of selectors indexed with the cache key returned by a custom resolver function. +Enhances Reselect selectors by wrapping [`createSelector`] and returning a memoized collection of selectors indexed with the cache key returned by a custom resolver function. Useful to reduce selectors recalculation when the same selector is repeatedly called with one/few different arguments. @@ -1802,7 +1801,7 @@ Useful to reduce selectors recalculation when the same selector is repeatedly ca [Flipper plugin](https://github.com/vlanemcev/flipper-plugin-reselect-debugger) and [and the connect app](https://github.com/vlanemcev/reselect-debugger-flipper) for debugging selectors in **React Native Apps**. -Inspired by `Reselect Tools`, so it also has all functionality from this library and more, but only for React Native and Flipper. +Inspired by Reselect Tools, so it also has all functionality from this library and more, but only for React Native and Flipper. - Selectors Recomputations count in live time across the App for identify performance bottlenecks - Highlight most recomputed selectors @@ -1845,17 +1844,19 @@ Originally inspired by getters in [NuclearJS](https://github.com/optimizely/nucl [`useMemo`]: https://react.dev/reference/react/useMemo#usememo 'useMemo' [`useCallback`]: https://react.dev/reference/react/useCallback#usecallback 'useCallback' [`re-reselect`]: https://github.com/toomuchdesign/re-reselect 're-reselect' +[Redux]: https://redux.js.org 'Redux' +[React]: https://react.dev 'React' +[React-Redux]: https://react-redux.js.org 'React-Redux' -[`selector`]: #selector-function 'Selector Function' -[`input selectors`]: #input-selectors 'Input Selectors' -[`output selector`]: #output-selector 'Output Selector' -[`result function`]: #result-function 'Result Function' -[`combiner`]: #combiner 'Combiner' +[selector]: #selector-function 'Selector Function' +[input selectors]: #input-selectors 'Input Selectors' +[output selector]: #output-selector 'Output Selector' +[result function]: #result-function 'Result Function' [`dependencies`]: #dependencies 'Dependencies' [**_Cascading Memoization_**]: #cascading-memoization 'Cascading Memoization' -[`output selector fields`]: #output-selector-fields 'Output Selector Fields' +[output selector fields]: #output-selector-fields 'Output Selector Fields' [`createSelector`]: #createselectorinputselectors--inputselectors-resultfunc-createselectoroptions 'createSelector' [`createSelectorCreator`]: #createselectorcreatormemoize--options-memoizeoptions 'createSelectorCreator' [`defaultMemoize`]: #defaultmemoizefunc-equalitycheckoroptions--defaultequalitycheck 'defaultMemoize' From bdf43e5e5c554e63dfd69520aa3a7e2ad0bf310e Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 15 Nov 2023 04:11:24 -0600 Subject: [PATCH 19/50] Fix API collapsed sections --- README.md | 218 +++++++++++++++++++++++++----------------------------- 1 file changed, 99 insertions(+), 119 deletions(-) diff --git a/README.md b/README.md index 9b777288b..89923bc4d 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ In addition to skipping unnecessary recalculations, `memoizedSelectCompletedTodo - [How Does Reselect Work?](#how-does-reselect-work) - [Cascading Memoization](#cascading-memoization) - [Reselect Vs Standard Memoization](#reselect-vs-standard-memoization) - - [Why Reselect Is Often Used With [Redux]](#why-reselect-is-often-used-with-redux) + - [Why Reselect Is Often Used With Redux](#why-reselect-is-often-used-with-redux) - [API](#api) - [**`createSelector`**][`createSelector`] - [**`defaultMemoize`**][`defaultMemoize`] @@ -134,7 +134,7 @@ In addition to skipping unnecessary recalculations, `memoizedSelectCompletedTodo - [FAQ](#faq) - [Why isn’t my selector recomputing when the input state changes?](#why-isnt-my-selector-recomputing-when-the-input-state-changes) - [Why is my selector recomputing when the input state stays the same?](#why-is-my-selector-recomputing-when-the-input-state-stays-the-same) - - [Can I use Reselect without [Redux]?](#can-i-use-reselect-without-redux) + - [Can I use Reselect without Redux?](#can-i-use-reselect-without-redux) - [How do I create a selector that takes an argument?](#how-do-i-create-a-selector-that-takes-an-argument) - [The default memoization function is no good, can I use a different one?](#the-default-memoization-function-is-no-good-can-i-use-a-different-one) - [How do I test a selector?](#how-do-i-test-a-selector) @@ -194,7 +194,7 @@ The way Reselect works can be broken down into multiple parts: - If they're the same, it returns the cached result without running the [input selectors] or the [result function]. - - If they differ, it proceeds to the second level. + - If they differ, it proceeds ("cascades") to the second level. - **Second Level**: It runs the [input selectors] and compares their current results with the previous ones (done by `memoize`). > [!NOTE] @@ -295,15 +295,13 @@ Even when the overall `state` changes, Reselect ensures efficient memoization th ### createSelector(...inputSelectors | [inputSelectors], resultFunc, createSelectorOptions?) -
Description +Description Accepts one or more "[input selectors]" (either as separate arguments or a single array), a single "[result function]", and an optional options object, and generates a memoized selector function. -
- -
Parameters +Parameters | Name | Description | | :----------------------- | :-------------------------------------------------------------------------------- | @@ -311,14 +309,10 @@ generates a memoized selector function. | `resultFunc` | A function that takes the results of the [input selectors] as separate arguments. | | `createSelectorOptions?` | An optional options object that allows for further customization per selector. | -
- -
Returns +Returns A memoized [output selector]. -
-
Type parameters | Name | Description | @@ -340,13 +334,13 @@ A memoized [output selector]. #### defaultMemoize(func, equalityCheckOrOptions = defaultEqualityCheck) -
Description +Description The standard memoize function used by [`createSelector`]. -It has a default cache size of 1. This means it always recalculates when the value of an argument changes. However, this can be customized as needed with a specific max cache size (**`Since`** 4.1.0). +It has a default cache size of 1. This means it always recalculates when the value of an argument changes. However, this can be customized as needed with a specific max cache size (since 4.1.0). -It determines if an argument has changed by calling the `equalityCheck` function. As `defaultMemoize` is designed to be used with immutable data, the default `equalityCheck` function checks for changes using [`reference equality`][Reference Equality Check]: +It determines if an argument has changed by calling the `equalityCheck` function. As `defaultMemoize` is designed to be used with immutable data, the default `equalityCheck` function checks for changes using [reference equality][Reference Equality Check]: ```ts const defaultEqualityCheck = (previousValue: any, currentValue: any) => { @@ -354,16 +348,14 @@ const defaultEqualityCheck = (previousValue: any, currentValue: any) => { } ``` -
- -
Parameters +Parameters | Name | Description | | :----------------------- | :---------------------------------------------------------- | | `func` | The function to be memoized. | | `equalityCheckOrOptions` | Either an `equality check` function or an `options` object. | -**`Since`** 4.1.0, `defaultMemoize` also accepts an options object as its first argument instead of an `equalityCheck` function. The `options` object may contain: +Since 4.1.0, `defaultMemoize` also accepts an options object as its first argument instead of an `equalityCheck` function. The `options` object may contain: ```ts type EqualityFn = (a: any, b: any) => boolean @@ -384,14 +376,10 @@ interface DefaultMemoizeOptions { > [!WARNING] > If `resultEqualityCheck` is used inside `argsMemoizeOptions` it has no effect. -
- -
Returns +Returns A memoized function with a `.clearCache()` method attached. -
-
Type parameters | Name | Description | @@ -400,7 +388,7 @@ A memoized function with a `.clearCache()` method attached.
-
Examples +
Examples ###### Using [`createSelector`] @@ -461,16 +449,14 @@ const selectTodoIds = createSelectorShallowEqual( -#### weakMapMemoize(func) - (**`Since`** 5.0.0) +#### weakMapMemoize(func) - (since 5.0.0) -
Description +Description [`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 selectors). **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. -
-
Design Tradeoffs - Pros: @@ -494,20 +480,16 @@ useSelector(state => selectSomeData(state, id))
-
Parameters +Parameters | Name | Description | | :----- | :--------------------------- | | `func` | The function to be memoized. | -
- -
Returns +Returns A memoized function with a `.clearCache()` method attached. -
-
Type parameters | Name | Description | @@ -516,20 +498,27 @@ A memoized function with a `.clearCache()` method attached.
-
Examples +
Examples Prior to `weakMapMemoize`, you had this problem: ```ts -const parametricSelector = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.filter(todo => todo.id === id) +interface RootState { + items: { id: number; category: string; name: string }[] +} + +const selectItemsByCategory = createSelector( + [ + (state: RootState) => state.items, + (state: RootState, category: string) => category + ], + (items, category) => items.filter(item => item.category === category) ) -parametricSelector(state, 0) // Selector runs -parametricSelector(state, 0) -parametricSelector(state, 1) // Selector runs -parametricSelector(state, 0) // Selector runs again! +selectItemsByCategory(state, 'Electronics') // Selector runs +selectItemsByCategory(state, 'Electronics') +selectItemsByCategory(state, 'Stationery') // Selector runs +selectItemsByCategory(state, 'Electronics') // Selector runs again! ``` Before you could solve this in a number of different ways: @@ -537,9 +526,12 @@ Before you could solve this in a number of different ways: 1. Set the `maxSize` with [`defaultMemoize`]: ```ts -const parametricSelector = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.filter(todo => todo.id === id), +const selectItemsByCategory = createSelector( + [ + (state: RootState) => state.items, + (state: RootState, category: string) => category + ], + (items, category) => items.filter(item => item.category === category), { memoizeOptions: { maxSize: 10 @@ -553,20 +545,27 @@ But this required having to know the cache size ahead of time. 2. Create unique selector instances using [`useMemo`]. ```tsx -const parametricSelector = (id: number) => - createSelector([(state: RootState) => state.todos], todos => - todos.filter(todo => todo.id === id) +const makeSelectItemsByCategory = (category: string) => + createSelector([(state: RootState) => state.items], items => + items.filter(item => item.category === category) ) -const MyComponent: FC = ({ id }) => { - const selectTodosById = useMemo(() => parametricSelector(id), [id]) +interface Props { + category: string +} + +const MyComponent: FC = ({ category }) => { + const selectItemsByCategory = useMemo( + () => makeSelectItemsByCategory(category), + [category] + ) - const todosById = useSelector(selectTodosById) + const itemsByCategory = useSelector(selectItemsByCategory) return (
- {todosById.map(todo => ( -
{todo.title}
+ {itemsByCategory.map(item => ( +
{item.name}
))}
) @@ -576,15 +575,18 @@ const MyComponent: FC = ({ id }) => { 3. Using [`useCallback`]. ```tsx -const parametricSelector = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.filter(todo => todo.id === id) +const selectItemsByCategory = createSelector( + [ + (state: RootState) => state.items, + (state: RootState, category: string) => category + ], + (items, category) => items.filter(item => item.category === category) ) const MyComponent: FC = ({ id }) => { - const selectTodosById = useCallback(parametricSelector, []) + const selectTodosById = useCallback(selectItemsByCategory, []) - const todosById = useSelector(state => selectTodosById(state, id)) + const todosById = useSelector(state => selectItemsByCategory(state, id)) return (
@@ -601,7 +603,7 @@ const MyComponent: FC = ({ id }) => { ```ts import { createCachedSelector } from 're-reselect' -const parametricSelector = createCachedSelector( +const selectItemsByCategory = createCachedSelector( [(state: RootState) => state.todos, (state: RootState, id: number) => id], (todos, id) => todos.filter(todo => todo.id === id) )((state: RootState, id: number) => id) @@ -612,7 +614,7 @@ Starting in 5.0.0, you can eliminate this problem using `weakMapMemoize`. ###### Using [`createSelector`] ```ts -const parametricSelector = createSelector( +const selectItemsByCategory = createSelector( [(state: RootState) => state.todos, (state: RootState, id: number) => id], (todos, id) => todos.filter(todo => todo.id === id), { @@ -621,10 +623,10 @@ const parametricSelector = createSelector( } ) -parametricSelector(state, 0) // Selector runs -parametricSelector(state, 0) -parametricSelector(state, 1) // Selector runs -parametricSelector(state, 0) +selectItemsByCategory(state, 0) // Selector runs +selectItemsByCategory(state, 0) +selectItemsByCategory(state, 1) // Selector runs +selectItemsByCategory(state, 0) ``` ###### Using [`createSelectorCreator`] @@ -658,16 +660,13 @@ This solves the problem of having to know and set the cache size prior to creati -#### autotrackMemoize(func) - (**`Since`** 5.0.0) +#### autotrackMemoize(func) - (since 5.0.0) -
Description +Description 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. -
- -
-Design Tradeoffs +
Design Tradeoffs - Pros: - It is likely to avoid excess calculations and recalculate fewer times than defaultMemoize will, which may also result in fewer component re-renders. @@ -685,27 +684,22 @@ Uses an "auto-tracking" approach inspired by the work of the Ember Glimmer team.
-
-Use Cases +
Use Cases - It 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.
-
Parameters +Parameters | Name | Description | | :----- | :--------------------------- | | `func` | The function to be memoized. | -
- -
Returns +Returns A memoized function with a `.clearCache()` method attached. -
-
Type parameters | Name | Description | @@ -714,7 +708,7 @@ A memoized function with a `.clearCache()` method attached.
-
Examples +
Examples ###### Using [`createSelector`] @@ -767,7 +761,7 @@ const selectTodoIds = createSelectorAutotrack( - It 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. -
--> +
@@ -777,40 +771,32 @@ const selectTodoIds = createSelectorAutotrack( ### createSelectorCreator(memoize | options, ...memoizeOptions) -
Description +Description -Accepts either a `memoize` function and `...memoizeOptions` rest parameter, or **`Since`** 5.0.0 an `options` object containing a `memoize` function and creates a custom selector creator function. +Accepts either a `memoize` function and `...memoizeOptions` rest parameter, or since 5.0.0 an `options` object containing a `memoize` function and creates a custom selector creator function. -
- -
Parameters (Since 5.0.0) +Parameters (since 5.0.0) -| Name | Description | -| :----------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `options` | An options object containing the `memoize` function responsible for memoizing the `resultFunc` inside [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). It also provides additional options for customizing memoization. While the `memoize` property is mandatory, the rest are optional. | -| `options.argsMemoize?` | The optional memoize function that is used to memoize the arguments passed into the [output selector] generated by [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`).
**`Default`** `defaultMemoize` | -| `options.argsMemoizeOptions?` | Optional configuration options for the `argsMemoize` function. These options are passed to the `argsMemoize` function as the second argument.
**`Since`** 5.0.0 | -| `options.inputStabilityCheck?` | Overrides the global input stability check for the selector. Possible values are:
`once` - Run only the first time the selector is called.
`always` - Run every time the selector is called.
`never` - Never run the input stability check.
**`Default`** = `'once'`
**`Since`** 5.0.0 | -| `options.memoize` | The memoize function that is used to memoize the `resultFunc` inside [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). **`Since`** 5.0.0 | -| `options.memoizeOptions?` | Optional configuration options for the `memoize` function. These options are passed to the `memoize` function as the second argument.
**`Since`** 5.0.0 | +| Name | Description | +| :----------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `options` | An options object containing the `memoize` function responsible for memoizing the `resultFunc` inside [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). It also provides additional options for customizing memoization. While the `memoize` property is mandatory, the rest are optional. | +| `options.argsMemoize?` | The optional memoize function that is used to memoize the arguments passed into the [output selector] generated by [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`).
**`Default`** `defaultMemoize` | +| `options.argsMemoizeOptions?` | Optional configuration options for the `argsMemoize` function. These options are passed to the `argsMemoize` function as the second argument.
since 5.0.0 | +| `options.inputStabilityCheck?` | Overrides the global input stability check for the selector. Possible values are:
`once` - Run only the first time the selector is called.
`always` - Run every time the selector is called.
`never` - Never run the input stability check.
**`Default`** = `'once'`
since 5.0.0 | +| `options.memoize` | The memoize function that is used to memoize the `resultFunc` inside [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). since 5.0.0 | +| `options.memoizeOptions?` | Optional configuration options for the `memoize` function. These options are passed to the `memoize` function as the second argument.
since 5.0.0 | -
- -
Parameters +Parameters | Name | Description | | :-------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- | | `memoize` | The `memoize` function responsible for memoizing the `resultFunc` inside [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). | | `...memoizeOptionsFromArgs` | Optional configuration options for the memoization function. These options are then passed to the memoize function as the second argument onwards. | -
- -
Returns +Returns A customized [`createSelector`] function. -
-
Type parameters | Name | Description | @@ -820,9 +806,9 @@ A customized [`createSelector`] function.
-
Examples +
Examples -##### Using `options` (**`Since`** 5.0.0) +##### Using `options` (since 5.0.0) ```ts const customCreateSelector = createSelectorCreator({ @@ -919,29 +905,23 @@ const selector = customSelectorCreator( ### createStructuredSelector({ inputSelectors }, selectorCreator = createSelector) -
Description +Description A convenience function for a common pattern that arises when using Reselect. The selector passed to a `connect` decorator often just takes the values of its [input selectors] and maps them to keys in an object. -
- -
Parameters +Parameters | Name | Description | | :----------------- | :--------------------------------------------------------------------- | | `selectorMap` | A key value pair consisting of input selectors. | | `selectorCreator?` | A custom selector creator function. It defaults to [`createSelector`]. | -
- -
Returns +Returns A memoized structured selector. -
-
Type parameters | Name | Description | @@ -952,7 +932,7 @@ A memoized structured selector.
-
Examples +
Examples ##### Modern Use Case @@ -1030,7 +1010,7 @@ const result = structuredSelector({ a: 1, b: 2 }) // will produce { x: 1, y: 2 } -
Development-only Checks - (Since 5.0.0) +
Development-only Checks - (since 5.0.0) @@ -1059,10 +1039,10 @@ type StabilityCheckFrequency = 'always' | 'once' | 'never' | :-------------- | :---------------------------------------------- | | `once` | Run only the first time the selector is called. | | `always` | Run every time the selector is called. | -| `never` | Never run the `input stability check`. | +| `never` | Never run the input stability check. | > [!IMPORTANT] -> The `input stability check` is automatically disabled in production environments. +> The input stability check is automatically disabled in production environments. You can configure this behavior in two ways: @@ -1102,7 +1082,7 @@ const selectCompletedTodosLength = createSelector( ``` > [!WARNING] -> This will override the global `input stability check` set by calling `setInputStabilityCheckEnabled`. +> This will override the global input stability check set by calling `setInputStabilityCheckEnabled`.
@@ -1116,7 +1096,7 @@ const selectCompletedTodosLength = createSelector( | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `resultFunc` | The final function passed to [`createSelector`]. | | `memoizedResultFunc` | The memoized version of `resultFunc`. | -| `lastResult` | Returns The last result calculated by `memoizedResultFunc`. | +| `lastResult` | Returns the last result calculated by `memoizedResultFunc`. | | `dependencies` | The array of the input selectors used by [`createSelector`] to compose `resultFunc`. | | `recomputations` | Counts the number of times `memoizedResultFunc` has been recalculated. | | `resetRecomputations` | Resets the count of `recomputations` count to 0. | From 0112f1afa24c3c0b32cf315f7c182d860676d909 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 15 Nov 2023 06:44:20 -0600 Subject: [PATCH 20/50] Remove `ParametricSelector` and `OutputParametricSelector` --- README.md | 4 ++++ src/index.ts | 2 -- src/types.ts | 27 --------------------------- 3 files changed, 4 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 89923bc4d..a73737178 100644 --- a/README.md +++ b/README.md @@ -1145,6 +1145,10 @@ const selectCompletedTodosLength = createSelector( These updates aim to enhance flexibility, performance, and developer experience. For detailed usage and examples, refer to the updated documentation sections for each feature. +- **Breaking Changes**: + + - Removed `ParametricSelector` and `OutputParametricSelector` types. Their functionalities are now integrated into `Selector` and `OutputSelector` respectively, which inherently support additional parameters. +
diff --git a/src/index.ts b/src/index.ts index a61ada271..dde6f5ae4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,11 +27,9 @@ export type { GetParamsFromSelectors, GetStateFromSelectors, MemoizeOptionsFromParameters, - OutputParametricSelector, OutputSelector, OutputSelectorFields, OverrideMemoizeOptions, - ParametricSelector, Selector, SelectorArray, SelectorResultArray, diff --git a/src/types.ts b/src/types.ts index 5ac812ca7..3ec3f0404 100644 --- a/src/types.ts +++ b/src/types.ts @@ -198,7 +198,6 @@ export type OutputSelectorFields< /** * The memoized version of {@linkcode OutputSelectorFields.resultFunc resultFunc}. - * */ memoizedResultFunc: Combiner & ExtractMemoizerFields @@ -216,13 +215,11 @@ export type OutputSelectorFields< /** * Counts the number of times {@linkcode OutputSelectorFields.memoizedResultFunc memoizedResultFunc} has been recalculated. - * */ recomputations: () => number /** * Resets the count of {@linkcode OutputSelectorFields.recomputations recomputations} count to 0. - * */ resetRecomputations: () => 0 @@ -298,30 +295,6 @@ export type Combiner = Distribute< (...resultFuncArgs: SelectorResultArray) => Result > -/** - * A selector that is assumed to have one additional argument, such as - * the props from a React component. - * - * @public - */ -export type ParametricSelector = Selector< - State, - Result, - [Props, ...any] -> - -/** - * A generated selector that is assumed to have one additional argument. - * - * @public - */ -export type OutputParametricSelector = ParametricSelector< - State, - Props, - Result -> & - OutputSelectorFields - /** * A standard function returning true if two values are considered equal. * From 9f7c28824765f34fe575aa7b6e9226e09b46af50 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 15 Nov 2023 06:46:04 -0600 Subject: [PATCH 21/50] Remove `ParametricSelector` and `OutputParametricSelector` from type tests. --- typescript_test/test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/typescript_test/test.ts b/typescript_test/test.ts index 379768f60..7f674d320 100644 --- a/typescript_test/test.ts +++ b/typescript_test/test.ts @@ -9,7 +9,7 @@ import type { TypedUseSelectorHook } from 'react-redux' import { useSelector } from 'react-redux' import type { GetStateFromSelectors, - ParametricSelector, + Selector, SelectorResultArray, TypedStructuredSelectorCreator } from 'reselect' @@ -142,7 +142,7 @@ function testSelectorAsCombiner() { type Component

= (props: P) => any declare function connect( - selector: ParametricSelector + selector: Selector ): (component: Component

) => Component

function testConnect() { @@ -830,14 +830,14 @@ function testTypedCreateStructuredSelector() { const selectBar = (state: RootState) => state.bar const typedStructuredSelectorCreator: TypedStructuredSelectorCreator = - createStructuredSelector as TypedStructuredSelectorCreator + createStructuredSelector typedStructuredSelectorCreator({ foo: selectFoo, bar: selectBar }) - // @ts-expect-error + // @ts-expect-error Because `bar` is missing. typedStructuredSelectorCreator({ foo: selectFoo }) From 76ad4facdb8558d516ba82e64dc49018e41cc21f Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 15 Nov 2023 08:00:29 -0600 Subject: [PATCH 22/50] Remove `connect` reference from `createStructuredSelector` description in docs/ --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index a73737178..1c4df073d 100644 --- a/README.md +++ b/README.md @@ -907,9 +907,7 @@ const selector = customSelectorCreator( Description -A convenience function for a common pattern that arises when using Reselect. -The selector passed to a `connect` decorator often just takes the values of its [input selectors] -and maps them to keys in an object. +A convenience function that simplifies returning an object made up of selector results. Parameters From dfe31f4d5439d54be3cbb8dcb9d519d81aafe20a Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 15 Nov 2023 08:11:25 -0600 Subject: [PATCH 23/50] Move `createSelectorCreator` to be directly after `createSelector` in docs. --- README.md | 472 +++++++++++++++++++++++++++--------------------------- 1 file changed, 236 insertions(+), 236 deletions(-) diff --git a/README.md b/README.md index 1c4df073d..d6daaefa8 100644 --- a/README.md +++ b/README.md @@ -328,6 +328,241 @@ A memoized [output selector]. --- + + +### createSelectorCreator(memoize | options, ...memoizeOptions) + +Description + +Accepts either a `memoize` function and `...memoizeOptions` rest parameter, or since 5.0.0 an `options` object containing a `memoize` function and creates a custom selector creator function. + +Parameters (since 5.0.0) + +| Name | Description | +| :----------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `options` | An options object containing the `memoize` function responsible for memoizing the `resultFunc` inside [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). It also provides additional options for customizing memoization. While the `memoize` property is mandatory, the rest are optional. | +| `options.argsMemoize?` | The optional memoize function that is used to memoize the arguments passed into the [output selector] generated by [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`).
**`Default`** `defaultMemoize` | +| `options.argsMemoizeOptions?` | Optional configuration options for the `argsMemoize` function. These options are passed to the `argsMemoize` function as the second argument.
since 5.0.0 | +| `options.inputStabilityCheck?` | Overrides the global input stability check for the selector. Possible values are:
`once` - Run only the first time the selector is called.
`always` - Run every time the selector is called.
`never` - Never run the input stability check.
**`Default`** = `'once'`
since 5.0.0 | +| `options.memoize` | The memoize function that is used to memoize the `resultFunc` inside [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). since 5.0.0 | +| `options.memoizeOptions?` | Optional configuration options for the `memoize` function. These options are passed to the `memoize` function as the second argument.
since 5.0.0 | + +Parameters + +| Name | Description | +| :-------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- | +| `memoize` | The `memoize` function responsible for memoizing the `resultFunc` inside [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). | +| `...memoizeOptionsFromArgs` | Optional configuration options for the memoization function. These options are then passed to the memoize function as the second argument onwards. | + +Returns + +A customized [`createSelector`] function. + +

Type parameters + +| Name | Description | +| :-------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `MemoizeFunction` | The type of the memoize function that is used to memoize the `resultFunc` inside [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). | +| `ArgsMemoizeFunction` | The type of the optional memoize function that is used to memoize the arguments passed into the [output selector] generated by [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used. | + +
+ +
Examples + +##### Using `options` (since 5.0.0) + +```ts +const customCreateSelector = createSelectorCreator({ + memoize: customMemoize, // Function to be used to memoize `resultFunc` + memoizeOptions: [memoizeOption1, memoizeOption2], // Options passed to `customMemoize` as the second argument onwards + argsMemoize: customArgsMemoize, // Function to be used to memoize the selector's arguments + argsMemoizeOptions: [argsMemoizeOption1, argsMemoizeOption2] // Options passed to `customArgsMemoize` as the second argument onwards +}) + +const customSelector = customCreateSelector( + [inputSelector1, inputSelector2], + resultFunc // `resultFunc` will be passed as the first argument to `customMemoize` +) + +customSelector( + ...selectorArgs // Will be memoized by `customArgsMemoize` +) +``` + + + +--- + +##### Using `memoize` and `...memoizeOptions` + +`createSelectorCreator` can be used to make a customized version of [`createSelector`]. + +The `memoize` argument is a memoization function to replace `defaultMemoize`. + +The `...memoizeOptions` rest parameters are zero or more configuration options to be passed to `memoizeFunc`. The selectors `resultFunc` is passed as the first argument to `memoize` and the `memoizeOptions` are passed as the second argument onwards: + +```ts +const customSelectorCreator = createSelectorCreator( + customMemoize, // Function to be used to memoize `resultFunc` + option1, // `option1` will be passed as second argument to `customMemoize` + option2, // `option2` will be passed as third argument to `customMemoize` + option3 // `option3` will be passed as fourth argument to `customMemoize` +) + +const customSelector = customSelectorCreator( + [inputSelector1, inputSelector2], + resultFunc // `resultFunc` will be passed as first argument to `customMemoize` +) +``` + +Internally `customSelector` calls the memoize function as follows: + +```ts +customMemoize(resultFunc, option1, option2, option3) +``` + +##### Additional Examples + +###### Customize `equalityCheck` for `defaultMemoize` + +```js +import { createSelectorCreator, defaultMemoize } from 'reselect' +import isEqual from 'lodash.isequal' + +// create a "selector creator" that uses lodash.isequal instead of === +const createDeepEqualSelector = createSelectorCreator(defaultMemoize, isEqual) + +// use the new "selector creator" to create a selector +const selectSum = createDeepEqualSelector( + [state => state.values.filter(val => val < 5)], + values => values.reduce((acc, val) => acc + val, 0) +) +``` + +###### Use memoize function from `Lodash` for an unbounded cache + +```js +import { createSelectorCreator } from 'reselect' +import memoize from 'lodash.memoize' + +const hashFn = (...args) => + args.reduce((acc, val) => acc + '-' + JSON.stringify(val), '') + +const customSelectorCreator = createSelectorCreator(memoize, hashFn) + +const selector = customSelectorCreator( + [state => state.a, state => state.b], + (a, b) => a + b +) +``` + +
+ + + +--- + + + +### createStructuredSelector({ inputSelectors }, selectorCreator = createSelector) + +Description + +A convenience function that simplifies returning an object made up of selector results. + +Parameters + +| Name | Description | +| :----------------- | :--------------------------------------------------------------------- | +| `selectorMap` | A key value pair consisting of input selectors. | +| `selectorCreator?` | A custom selector creator function. It defaults to [`createSelector`]. | + +Returns + +A memoized structured selector. + +
Type parameters + +| Name | Description | +| :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `InputSelectorsObject` | The shape of the [input selectors] object. | +| `MemoizeFunction` | The type of the memoize function that is used to create the structured selector. It defaults to `defaultMemoize`. | +| `ArgsMemoizeFunction` | The type of the of the memoize function that is used to memoize the arguments passed into the generated structured selector. It defaults to `defaultMemoize`. | + +
+ +
Examples + +##### Modern Use Case + +```ts +import { createSelector, createStructuredSelector } from 'reselect' + +interface RootState { + todos: { id: number; completed: boolean }[] + alerts: { id: number; read: boolean }[] +} + +const state: RootState = { + todos: [ + { id: 0, completed: false }, + { id: 1, completed: true } + ], + alerts: [ + { id: 0, read: false }, + { id: 1, read: true } + ] +} + +// This: +const structuredSelector = createStructuredSelector( + { + allTodos: (state: RootState) => state.todos, + allAlerts: (state: RootState) => state.alerts, + selectedTodo: (state: RootState, id: number) => state.todos[id] + }, + createSelector +) + +// Is essentially the same as this: +const selector = createSelector( + [ + (state: RootState) => state.todos, + (state: RootState) => state.alerts, + (state: RootState, id: number) => state.todos[id] + ], + (allTodos, allAlerts, selectedTodo) => { + return { + allTodos, + allAlerts, + selectedTodo + } + } +) +``` + +##### Simple Use Case + +```ts +const selectA = state => state.a +const selectB = state => state.b + +// The result function in the following selector +// is simply building an object from the input selectors +const structuredSelector = createSelector(selectA, selectB, (a, b) => ({ + a, + b +})) + +const result = structuredSelector({ a: 1, b: 2 }) // will produce { x: 1, y: 2 } +``` + +
+ + + +--- + ### Memoization Functions @@ -767,241 +1002,6 @@ const selectTodoIds = createSelectorAutotrack( --- - - -### createSelectorCreator(memoize | options, ...memoizeOptions) - -Description - -Accepts either a `memoize` function and `...memoizeOptions` rest parameter, or since 5.0.0 an `options` object containing a `memoize` function and creates a custom selector creator function. - -Parameters (since 5.0.0) - -| Name | Description | -| :----------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `options` | An options object containing the `memoize` function responsible for memoizing the `resultFunc` inside [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). It also provides additional options for customizing memoization. While the `memoize` property is mandatory, the rest are optional. | -| `options.argsMemoize?` | The optional memoize function that is used to memoize the arguments passed into the [output selector] generated by [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`).
**`Default`** `defaultMemoize` | -| `options.argsMemoizeOptions?` | Optional configuration options for the `argsMemoize` function. These options are passed to the `argsMemoize` function as the second argument.
since 5.0.0 | -| `options.inputStabilityCheck?` | Overrides the global input stability check for the selector. Possible values are:
`once` - Run only the first time the selector is called.
`always` - Run every time the selector is called.
`never` - Never run the input stability check.
**`Default`** = `'once'`
since 5.0.0 | -| `options.memoize` | The memoize function that is used to memoize the `resultFunc` inside [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). since 5.0.0 | -| `options.memoizeOptions?` | Optional configuration options for the `memoize` function. These options are passed to the `memoize` function as the second argument.
since 5.0.0 | - -Parameters - -| Name | Description | -| :-------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- | -| `memoize` | The `memoize` function responsible for memoizing the `resultFunc` inside [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). | -| `...memoizeOptionsFromArgs` | Optional configuration options for the memoization function. These options are then passed to the memoize function as the second argument onwards. | - -Returns - -A customized [`createSelector`] function. - -
Type parameters - -| Name | Description | -| :-------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `MemoizeFunction` | The type of the memoize function that is used to memoize the `resultFunc` inside [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). | -| `ArgsMemoizeFunction` | The type of the optional memoize function that is used to memoize the arguments passed into the [output selector] generated by [`createSelector`] (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used. | - -
- -
Examples - -##### Using `options` (since 5.0.0) - -```ts -const customCreateSelector = createSelectorCreator({ - memoize: customMemoize, // Function to be used to memoize `resultFunc` - memoizeOptions: [memoizeOption1, memoizeOption2], // Options passed to `customMemoize` as the second argument onwards - argsMemoize: customArgsMemoize, // Function to be used to memoize the selector's arguments - argsMemoizeOptions: [argsMemoizeOption1, argsMemoizeOption2] // Options passed to `customArgsMemoize` as the second argument onwards -}) - -const customSelector = customCreateSelector( - [inputSelector1, inputSelector2], - resultFunc // `resultFunc` will be passed as the first argument to `customMemoize` -) - -customSelector( - ...selectorArgs // Will be memoized by `customArgsMemoize` -) -``` - - - ---- - -##### Using `memoize` and `...memoizeOptions` - -`createSelectorCreator` can be used to make a customized version of [`createSelector`]. - -The `memoize` argument is a memoization function to replace `defaultMemoize`. - -The `...memoizeOptions` rest parameters are zero or more configuration options to be passed to `memoizeFunc`. The selectors `resultFunc` is passed as the first argument to `memoize` and the `memoizeOptions` are passed as the second argument onwards: - -```ts -const customSelectorCreator = createSelectorCreator( - customMemoize, // Function to be used to memoize `resultFunc` - option1, // `option1` will be passed as second argument to `customMemoize` - option2, // `option2` will be passed as third argument to `customMemoize` - option3 // `option3` will be passed as fourth argument to `customMemoize` -) - -const customSelector = customSelectorCreator( - [inputSelector1, inputSelector2], - resultFunc // `resultFunc` will be passed as first argument to `customMemoize` -) -``` - -Internally `customSelector` calls the memoize function as follows: - -```ts -customMemoize(resultFunc, option1, option2, option3) -``` - -##### Additional Examples - -###### Customize `equalityCheck` for `defaultMemoize` - -```js -import { createSelectorCreator, defaultMemoize } from 'reselect' -import isEqual from 'lodash.isequal' - -// create a "selector creator" that uses lodash.isequal instead of === -const createDeepEqualSelector = createSelectorCreator(defaultMemoize, isEqual) - -// use the new "selector creator" to create a selector -const selectSum = createDeepEqualSelector( - [state => state.values.filter(val => val < 5)], - values => values.reduce((acc, val) => acc + val, 0) -) -``` - -###### Use memoize function from `Lodash` for an unbounded cache - -```js -import { createSelectorCreator } from 'reselect' -import memoize from 'lodash.memoize' - -const hashFn = (...args) => - args.reduce((acc, val) => acc + '-' + JSON.stringify(val), '') - -const customSelectorCreator = createSelectorCreator(memoize, hashFn) - -const selector = customSelectorCreator( - [state => state.a, state => state.b], - (a, b) => a + b -) -``` - -
- - - ---- - - - -### createStructuredSelector({ inputSelectors }, selectorCreator = createSelector) - -Description - -A convenience function that simplifies returning an object made up of selector results. - -Parameters - -| Name | Description | -| :----------------- | :--------------------------------------------------------------------- | -| `selectorMap` | A key value pair consisting of input selectors. | -| `selectorCreator?` | A custom selector creator function. It defaults to [`createSelector`]. | - -Returns - -A memoized structured selector. - -
Type parameters - -| Name | Description | -| :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `InputSelectorsObject` | The shape of the [input selectors] object. | -| `MemoizeFunction` | The type of the memoize function that is used to create the structured selector. It defaults to `defaultMemoize`. | -| `ArgsMemoizeFunction` | The type of the of the memoize function that is used to memoize the arguments passed into the generated structured selector. It defaults to `defaultMemoize`. | - -
- -
Examples - -##### Modern Use Case - -```ts -import { createSelector, createStructuredSelector } from 'reselect' - -interface RootState { - todos: { id: number; completed: boolean }[] - alerts: { id: number; read: boolean }[] -} - -const state: RootState = { - todos: [ - { id: 0, completed: false }, - { id: 1, completed: true } - ], - alerts: [ - { id: 0, read: false }, - { id: 1, read: true } - ] -} - -// This: -const structuredSelector = createStructuredSelector( - { - allTodos: (state: RootState) => state.todos, - allAlerts: (state: RootState) => state.alerts, - selectedTodo: (state: RootState, id: number) => state.todos[id] - }, - createSelector -) - -// Is essentially the same as this: -const selector = createSelector( - [ - (state: RootState) => state.todos, - (state: RootState) => state.alerts, - (state: RootState, id: number) => state.todos[id] - ], - (allTodos, allAlerts, selectedTodo) => { - return { - allTodos, - allAlerts, - selectedTodo - } - } -) -``` - -##### Simple Use Case - -```ts -const selectA = state => state.a -const selectB = state => state.b - -// The result function in the following selector -// is simply building an object from the input selectors -const structuredSelector = createSelector(selectA, selectB, (a, b) => ({ - a, - b -})) - -const result = structuredSelector({ a: 1, b: 2 }) // will produce { x: 1, y: 2 } -``` - -
- - - ---- - ## Debugging Tools @@ -1555,7 +1555,7 @@ Yes! Reselect is now written in TypeScript itself, so they should Just Work™.
Answer -**`Since`** 5.0.0 you should be able to nest up to 30 selectors, but in case you still run into this issue, you can refer to [this +Starting in 5.0.0 you should be able to nest up to 30 selectors, but in case you still run into this issue, you can refer to [this comment](https://github.com/reduxjs/reselect/issues/534#issuecomment-956708953) for a discussion of the problem, as relating to nested selectors. From 55361f36280390a76bb76dd743eeebee6f4c866b Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 15 Nov 2023 08:17:34 -0600 Subject: [PATCH 24/50] Remove duplicate explanation in the `autotrackMemoize` section in docs. --- README.md | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index d6daaefa8..ae9bf460b 100644 --- a/README.md +++ b/README.md @@ -904,11 +904,11 @@ Uses an "auto-tracking" approach inspired by the work of the Ember Glimmer team.
Design Tradeoffs - Pros: - - It is likely to avoid excess calculations and recalculate fewer times than defaultMemoize will, which may also result in fewer component re-renders. + - It is likely to avoid excess calculations and recalculate fewer times than `defaultMemoize` will, which may also result in fewer component re-renders. - Cons: - 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 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: ```ts @@ -978,24 +978,6 @@ const selectTodoIds = createSelectorAutotrack( ) ``` -- Pros: - - It is likely to avoid excess calculations and recalculate fewer times than defaultMemoize will, which may also result in fewer component re-renders. -- Cons: - - - 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: - - ```ts - 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. - -**Use Cases for `autotrackMemoize`:** - -- It 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. -
From b0fae9292c72c41ea1f2927ef6a4c0391a32b573 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 15 Nov 2023 08:26:10 -0600 Subject: [PATCH 25/50] Rename `autotrackMemoize` to `unstable_autotrackMemoize` in examples inside docs. --- README.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ae9bf460b..8849c3880 100644 --- a/README.md +++ b/README.md @@ -948,28 +948,22 @@ A memoized function with a `.clearCache()` method attached. ###### Using [`createSelector`] ```ts -import { - unstable_autotrackMemoize as autotrackMemoize, - createSelector -} from 'reselect' +import { unstable_autotrackMemoize, createSelector } from 'reselect' const selectTodoIds = createSelector( [(state: RootState) => state.todos], todos => todos.map(todo => todo.id), - { memoize: autotrackMemoize } + { memoize: unstable_autotrackMemoize } ) ``` ###### Using [`createSelectorCreator`] ```ts -import { - unstable_autotrackMemoize as autotrackMemoize, - createSelectorCreator -} from 'reselect' +import { unstable_autotrackMemoize, createSelectorCreator } from 'reselect' const createSelectorAutotrack = createSelectorCreator({ - memoize: autotrackMemoize + memoize: unstable_autotrackMemoize }) const selectTodoIds = createSelectorAutotrack( From af90f435d78e4ecdfcea54500a9fc7aed34a377e Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 15 Nov 2023 08:29:44 -0600 Subject: [PATCH 26/50] Add experimental warning label to `unstable_autotrackMemoize` in docs. --- README.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8849c3880..794f6370d 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ In addition to skipping unnecessary recalculations, `memoizedSelectCompletedTodo - [**`createSelector`**][`createSelector`] - [**`defaultMemoize`**][`defaultMemoize`] - [**`weakMapMemoize`**][`weakMapMemoize`] - - [**`autotrackMemoize`**][`autotrackMemoize`] + - [**`unstable_autotrackMemoize`**][`unstable_autotrackMemoize`] - [**`createSelectorCreator`**][`createSelectorCreator`] - [**`createStructuredSelector`**][`createStructuredSelector`] - [**`createCurriedSelector`**][`createCurriedSelector`] @@ -893,18 +893,23 @@ This solves the problem of having to know and set the cache size prior to creati --- - + -#### autotrackMemoize(func) - (since 5.0.0) +#### unstable_autotrackMemoize(func) - (since 5.0.0) Description -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. +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`. + +> [!WARNING] +> This API is still experimental and undergoing testing.
Design Tradeoffs - Pros: + - It is likely to avoid excess calculations and recalculate fewer times than `defaultMemoize` will, which may also result in fewer component re-renders. + - Cons: - It only has a cache size of 1. @@ -1098,7 +1103,7 @@ const selectCompletedTodosLength = createSelector( - **Memoization Functions**: - - Introduced new experimental memoization functions: `weakMapMemoize` and `autotrackMemoize`. + - Introduced new experimental memoization functions: `weakMapMemoize` and `unstable_autotrackMemoize`. - Incorporated `memoize` and `argsMemoize` into the [output selector fields] for debugging purposes. - **TypeScript Support and Performance**: @@ -1819,7 +1824,7 @@ Originally inspired by getters in [NuclearJS](https://github.com/optimizely/nucl [`createSelectorCreator`]: #createselectorcreatormemoize--options-memoizeoptions 'createSelectorCreator' [`defaultMemoize`]: #defaultmemoizefunc-equalitycheckoroptions--defaultequalitycheck 'defaultMemoize' [`weakMapMemoize`]: #weakmapmemoizefunc---since-500 'weakMapMemoize' -[`autotrackMemoize`]: #autotrackmemoizefunc---since-500 'autotrackMemoize' +[`unstable_autotrackMemoize`]: #unstable_autotrackmemoizefunc---since-500 'unstable_autotrackMemoize' [`createStructuredSelector`]: #createstructuredselector-inputselectors--selectorcreator--createselector 'createStructuredSelector' [`createCurriedSelector`]: #createcurriedselectorinputselectors--inputselectors-resultfunc-createselectoroptions 'createCurriedSelector' From 1a5c72baa5becf8a4db534e56c8ee00e9cf2465e Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 15 Nov 2023 09:04:23 -0600 Subject: [PATCH 27/50] Fix `weakMapMemoize` examples. --- README.md | 124 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 78 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 794f6370d..5434a9753 100644 --- a/README.md +++ b/README.md @@ -695,10 +695,12 @@ const selectTodoIds = createSelectorShallowEqual(
Design Tradeoffs - Pros: + - 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 [`WeakMap`]s. + - Cons: - - There's currently no way to alter the argument comparisons. They're based on strict reference equality. + - There's currently no way to alter the argument comparisons. They're based on [strict reference equality][Reference Equality Check].
@@ -713,28 +715,6 @@ const selectTodoIds = createSelectorShallowEqual( useSelector(state => selectSomeData(state, id)) ``` -
- -Parameters - -| Name | Description | -| :----- | :--------------------------- | -| `func` | The function to be memoized. | - -Returns - -A memoized function with a `.clearCache()` method attached. - -
Type parameters - -| Name | Description | -| :----- | :----------------------------------------- | -| `Func` | The type of the function that is memoized. | - -
- -
Examples - Prior to `weakMapMemoize`, you had this problem: ```ts @@ -818,15 +798,17 @@ const selectItemsByCategory = createSelector( (items, category) => items.filter(item => item.category === category) ) -const MyComponent: FC = ({ id }) => { - const selectTodosById = useCallback(selectItemsByCategory, []) +const MyComponent: FC = ({ category }) => { + const selectItemsByCategoryMemoized = useCallback(selectItemsByCategory, []) - const todosById = useSelector(state => selectItemsByCategory(state, id)) + const itemsByCategory = useSelector(state => + selectItemsByCategoryMemoized(state, category) + ) return (
- {todosById.map(todo => ( -
{todo.title}
+ {itemsByCategory.map(item => ( +
{item.name}
))}
) @@ -839,29 +821,78 @@ const MyComponent: FC = ({ id }) => { import { createCachedSelector } from 're-reselect' const selectItemsByCategory = createCachedSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.filter(todo => todo.id === id) -)((state: RootState, id: number) => id) + [ + (state: RootState) => state.items, + (state: RootState, category: string) => category + ], + (items, category) => items.filter(item => item.category === category) +)((state: RootState, category: string) => category) ``` Starting in 5.0.0, you can eliminate this problem using `weakMapMemoize`. +```ts +const selectItemsByCategory = createSelector( + [ + (state: RootState) => state.items, + (state: RootState, category: string) => category + ], + (items, category) => items.filter(item => item.category === category), + { + memoize: weakMapMemoize, + argsMemoize: weakMapMemoize + } +) + +selectItemsByCategory(state, 'Electronics') // Selector runs +selectItemsByCategory(state, 'Electronics') +selectItemsByCategory(state, 'Stationery') // Selector runs +selectItemsByCategory(state, 'Electronics') +``` + +This solves the problem of having to know and set the cache size prior to creating a memoized selector. Because `weakMapMemoize` essentially provides a dynamic cache size out of the box. + +
+ +Parameters + +| Name | Description | +| :----- | :--------------------------- | +| `func` | The function to be memoized. | + +Returns + +A memoized function with a `.clearCache()` method attached. + +
Type parameters + +| Name | Description | +| :----- | :----------------------------------------- | +| `Func` | The type of the function that is memoized. | + +
+ +
Examples + ###### Using [`createSelector`] ```ts const selectItemsByCategory = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.filter(todo => todo.id === id), + [ + (state: RootState) => state.items, + (state: RootState, category: string) => category + ], + (items, category) => items.filter(item => item.category === category), { memoize: weakMapMemoize, argsMemoize: weakMapMemoize } ) -selectItemsByCategory(state, 0) // Selector runs -selectItemsByCategory(state, 0) -selectItemsByCategory(state, 1) // Selector runs -selectItemsByCategory(state, 0) +selectItemsByCategory(state, 'Electronics') // Selector runs +selectItemsByCategory(state, 'Electronics') +selectItemsByCategory(state, 'Stationery') // Selector runs +selectItemsByCategory(state, 'Electronics') ``` ###### Using [`createSelectorCreator`] @@ -874,19 +905,20 @@ const createSelectorWeakMap = createSelectorCreator({ argsMemoize: weakMapMemoize }) -const parametricSelector = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.filter(todo => todo.id === id) +const selectItemsByCategory = createSelectorWeakMap( + [ + (state: RootState) => state.items, + (state: RootState, category: string) => category + ], + (items, category) => items.filter(item => item.category === category) ) -parametricSelector(state, 0) // Selector runs -parametricSelector(state, 0) -parametricSelector(state, 1) // Selector runs -parametricSelector(state, 0) +selectItemsByCategory(state, 'Electronics') // Selector runs +selectItemsByCategory(state, 'Electronics') +selectItemsByCategory(state, 'Stationery') // Selector runs +selectItemsByCategory(state, 'Electronics') ``` -This solves the problem of having to know and set the cache size prior to creating a memoized selector. -
From 1768c206697200ae4bf758694c86861a9cb43d9c Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 15 Nov 2023 09:22:27 -0600 Subject: [PATCH 28/50] Fix JSDocs for `weakMapMemoize`. --- README.md | 4 +++- src/weakMapMemoize.ts | 25 ++++++++++++++++--------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 5434a9753..c3c7e9f4a 100644 --- a/README.md +++ b/README.md @@ -877,6 +877,8 @@ A memoized function with a `.clearCache()` method attached. ###### Using [`createSelector`] ```ts +import { createSelector, weakMapMemoize } from 'reselect' + const selectItemsByCategory = createSelector( [ (state: RootState) => state.items, @@ -1270,7 +1272,7 @@ There are a few details that will help you skip running as many functions as pos This: ```ts -// ✔️ This is optimal because we have less calculations in [`input selectors`][Input Selectors] and more in the `result function`. +// ✔️ This is optimal because we have less calculations in input selectors and more in the result function. const selectorGood = createSelector( [(state: RootState) => state.todos], todos => someExpensiveComputation(todos) diff --git a/src/weakMapMemoize.ts b/src/weakMapMemoize.ts index 6f5b079f7..33e783d9f 100644 --- a/src/weakMapMemoize.ts +++ b/src/weakMapMemoize.ts @@ -87,13 +87,20 @@ function createCacheNode(): CacheNode { * ```ts * import { createSelector, weakMapMemoize } from 'reselect' * - * const selectTodosById = createSelector( + * interface RootState { + * items: { id: number; category: string; name: string }[] + * } + * + * const selectItemsByCategory = createSelector( * [ - * (state: RootState) => state.todos, - * (state: RootState, id: number) => id + * (state: RootState) => state.items, + * (state: RootState, category: string) => category * ], - * (todos, id) => todos.filter(todo => todo.id === id), - * { memoize: weakMapMemoize } + * (items, category) => items.filter(item => item.category === category), + * { + * memoize: weakMapMemoize, + * argsMemoize: weakMapMemoize + * } * ) * ``` * @@ -104,12 +111,12 @@ function createCacheNode(): CacheNode { * * const createSelectorWeakMap = createSelectorCreator({ memoize: weakMapMemoize, argsMemoize: weakMapMemoize }) * - * const selectTodosById = createSelectorWeakMap( + * const selectItemsByCategory = createSelectorWeakMap( * [ - * (state: RootState) => state.todos, - * (state: RootState, id: number) => id + * (state: RootState) => state.items, + * (state: RootState, category: string) => category * ], - * (todos, id) => todos.filter(todo => todo.id === id) + * (items, category) => items.filter(item => item.category === category) * ) * ``` * From 191614cc0b9039bcf1eacec24f14f82ae8f159d3 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 15 Nov 2023 09:35:26 -0600 Subject: [PATCH 29/50] Tweak example titles for memoize functions in docs. --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c3c7e9f4a..b7cb192b1 100644 --- a/README.md +++ b/README.md @@ -625,7 +625,7 @@ A memoized function with a `.clearCache()` method attached.
Examples -###### Using [`createSelector`] +###### Using `defaultMemoize` with [`createSelector`] ```ts import { shallowEqual } from 'react-redux' @@ -649,7 +649,7 @@ const selectTodoIds = createSelector( ) ``` -###### Using [`createSelectorCreator`] +###### Using `defaultMemoize` with [`createSelectorCreator`] ```ts import { shallowEqual } from 'react-redux' @@ -874,7 +874,7 @@ A memoized function with a `.clearCache()` method attached.
Examples -###### Using [`createSelector`] +###### Using `weakMapMemoize` with [`createSelector`] ```ts import { createSelector, weakMapMemoize } from 'reselect' @@ -897,7 +897,7 @@ selectItemsByCategory(state, 'Stationery') // Selector runs selectItemsByCategory(state, 'Electronics') ``` -###### Using [`createSelectorCreator`] +###### Using `weakMapMemoize` with [`createSelectorCreator`] ```ts import { createSelectorCreator, weakMapMemoize } from 'reselect' @@ -984,7 +984,7 @@ A memoized function with a `.clearCache()` method attached.
Examples -###### Using [`createSelector`] +###### Using `unstable_autotrackMemoize` with [`createSelector`] ```ts import { unstable_autotrackMemoize, createSelector } from 'reselect' @@ -996,7 +996,7 @@ const selectTodoIds = createSelector( ) ``` -###### Using [`createSelectorCreator`] +###### Using `unstable_autotrackMemoize` with [`createSelectorCreator`] ```ts import { unstable_autotrackMemoize, createSelectorCreator } from 'reselect' From 917afa2dc619be57f858c0a030bc14984ce7f08d Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 15 Nov 2023 10:43:22 -0600 Subject: [PATCH 30/50] Replace `createCurriedSelector` API with copy paste pattern in docs. --- README.md | 85 ++---- src/createCurriedSelectorCreator.ts | 282 ------------------ src/index.ts | 6 - src/types.ts | 58 +--- test/autotrackMemoize.spec.ts | 1 - .../benchmarks/createCurriedSelector.bench.ts | 53 ---- type-tests/createCurriedSrlector.test-d.ts | 106 ------- 7 files changed, 38 insertions(+), 553 deletions(-) delete mode 100644 src/createCurriedSelectorCreator.ts delete mode 100644 test/benchmarks/createCurriedSelector.bench.ts delete mode 100644 type-tests/createCurriedSrlector.test-d.ts diff --git a/README.md b/README.md index b7cb192b1..dabe9f9d7 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,6 @@ In addition to skipping unnecessary recalculations, `memoizedSelectCompletedTodo - [**`unstable_autotrackMemoize`**][`unstable_autotrackMemoize`] - [**`createSelectorCreator`**][`createSelectorCreator`] - [**`createStructuredSelector`**][`createStructuredSelector`] - - [**`createCurriedSelector`**][`createCurriedSelector`] - [Debugging Tools](#debuggingtools) - [What's New in 5.0.0?](#v5summary) - [Optimizing Reselect](#optimizing-reselect) @@ -1147,7 +1146,6 @@ const selectCompletedTodosLength = createSelector( - **Selector API Enhancements**: - - Introduced experimental APIs: `createCurriedSelector` and `createCurriedSelectorCreator`, for more advanced selector patterns. - Removed the second overload of `createStructuredSelector` due to its susceptibility to runtime errors. - Added the `TypedStructuredSelectorCreator` utility type (_currently a work-in-progress_) to facilitate the creation of a pre-typed version of `createStructuredSelector` for your root state. @@ -1580,44 +1578,60 @@ relating to nested selectors.
Answer -You can try this new experimental API: +You can try this pattern: - +```ts +const currySelector = < + State, + Result, + Params extends readonly any[], + AdditionalFields +>( + selector: ((state: State, ...args: Params) => Result) & AdditionalFields +) => { + const curriedSelector = (...args: Params) => { + return (state: State) => { + return selector(state, ...args) + } + } + return Object.assign(curriedSelector, selector) +} -#### createCurriedSelector(...inputSelectors | [inputSelectors], resultFunc, createSelectorOptions?) +const selectTodoByIdCurried = currySelector( + createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id) + ) +) +``` This: ```ts -const parametricSelector = createSelector( +const selectTodoById = createSelector( [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.filter(todo => todo.id === id) + (todos, id) => todos.find(todo => todo.id === id) ) -parametricSelector(state, 0) +selectTodoById(state, 0) ``` Is the same as this: ```ts -const curriedSelector = createCurriedSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.filter(todo => todo.id === id) -) - -curriedSelector(0)(state) +selectTodoByIdCurried(0)(state) ``` As before you had to do this: ```ts -const selectTodo = useSelector(state => parametricSelector(state, id)) +const todoById = useSelector(state => selectTodoById(state, id)) ``` Now you can do this: ```ts -const selectTodo = useSelector(curriedSelector(id)) +const todoById = useSelector(selectTodoByIdCurried(id)) ``` Another thing you can do if you are using [React-Redux] is create a custom hook factory function: @@ -1652,44 +1666,6 @@ const MyComponent: FC = ({ id }) => { } ``` -We're also exporting a `createCurriedSelectorCreator` function for ease of use: - -```ts -import { - createCurriedSelectorCreator, - createSelector, - weakMapMemoize -} from 'reselect' - -const parametricSelector = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.filter(todo => todo.id === id), - { memoize: weakMapMemoize, argsMemoize: weakMapMemoize } -) - -const createCurriedSelectorWeakMap = createCurriedSelectorCreator({ - memoize: weakMapMemoize, - argsMemoize: weakMapMemoize -}) - -const curriedSelector = createCurriedSelectorWeakMap( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.filter(todo => todo.id === id) -) - -// This: -parametricSelector(state, 0) - -// Is the same as this: -curriedSelector(0)(state) - -// Inside your component you can replace this: -const selectTodo = useSelector(state => parametricSelector(state, id)) - -// With this: -const selectTodo = useSelector(curriedSelector(id)) -``` -
### How can I make pre-typed version of [`createSelector`](#createselectorinputselectors--inputselectors-resultfunc-createselectoroptions) for my root state? @@ -1860,7 +1836,6 @@ Originally inspired by getters in [NuclearJS](https://github.com/optimizely/nucl [`weakMapMemoize`]: #weakmapmemoizefunc---since-500 'weakMapMemoize' [`unstable_autotrackMemoize`]: #unstable_autotrackmemoizefunc---since-500 'unstable_autotrackMemoize' [`createStructuredSelector`]: #createstructuredselector-inputselectors--selectorcreator--createselector 'createStructuredSelector' -[`createCurriedSelector`]: #createcurriedselectorinputselectors--inputselectors-resultfunc-createselectoroptions 'createCurriedSelector'
diff --git a/src/createCurriedSelectorCreator.ts b/src/createCurriedSelectorCreator.ts deleted file mode 100644 index 87be70841..000000000 --- a/src/createCurriedSelectorCreator.ts +++ /dev/null @@ -1,282 +0,0 @@ -import type { CreateSelectorFunction } from './createSelectorCreator' -import { createSelectorCreator } from './createSelectorCreator' - -import { defaultMemoize } from './defaultMemoize' -import type { - Combiner, - CreateSelectorOptions, - CurriedOutputSelector, - DropFirstParameter, - InterruptRecursion, - SelectorArray, - Simplify, - UnknownMemoizer -} from './types' - -/** - * An instance of `createCurriedSelector`, customized with a given memoize implementation. - * - * @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createCurriedSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). - * @template ArgsMemoizeFunction - The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createCurriedSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used. - * - * @since 5.0.0 - * @public - * @experimental - */ -export interface CreateCurriedSelector< - MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, - ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize -> { - /** - * Creates a memoized "curried" selector function. - * - * @param createSelectorArgs - An arbitrary number of input selectors as separate inline arguments and a `combiner` function. - * @returns A memoized "curried" output selector. - * - * @template InputSelectors - The type of the input selectors as an array. - * @template Result - The return type of the `combiner` as well as the output selector. - * @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`. - * @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`. - * - * @since 5.0.0 - * @public - * @experimental - */ - ( - ...createSelectorArgs: [ - ...inputSelectors: InputSelectors, - combiner: Combiner - ] - ): CurriedOutputSelector< - InputSelectors, - Result, - MemoizeFunction, - ArgsMemoizeFunction - > & - InterruptRecursion - - /** - * Creates a memoized "curried" selector function. - * - * @param createSelectorArgs - An arbitrary number of input selectors as separate inline arguments, a `combiner` function and an `options` object. - * @returns A memoized "curried" output selector. - * - * @template InputSelectors - The type of the input selectors as an array. - * @template Result - The return type of the `combiner` as well as the output selector. - * @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`. - * @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`. - * - * @since 5.0.0 - * @public - * @experimental - */ - < - InputSelectors extends SelectorArray, - Result, - OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction, - OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction - >( - ...createSelectorArgs: [ - ...inputSelectors: InputSelectors, - combiner: Combiner, - createSelectorOptions: Simplify< - CreateSelectorOptions< - MemoizeFunction, - ArgsMemoizeFunction, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction - > - > - ] - ): CurriedOutputSelector< - InputSelectors, - Result, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction - > & - InterruptRecursion - - /** - * Creates a memoized "curried" selector function. - * - * @param inputSelectors - An array of input selectors. - * @param combiner - A function that Combines the input selectors and returns an output selector. Otherwise known as the result function. - * @param createSelectorOptions - An optional options object that allows for further customization per selector. - * @returns A memoized "curried" output selector. - * - * @template InputSelectors - The type of the input selectors array. - * @template Result - The return type of the `combiner` as well as the output selector. - * @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`. - * @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`. - * - * @since 5.0.0 - * @public - * @experimental - */ - < - InputSelectors extends SelectorArray, - Result, - OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction, - OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction - >( - inputSelectors: [...InputSelectors], - combiner: Combiner, - createSelectorOptions?: Simplify< - CreateSelectorOptions< - MemoizeFunction, - ArgsMemoizeFunction, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction - > - > - ): CurriedOutputSelector< - InputSelectors, - Result, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction - > & - InterruptRecursion -} -/** - * Creates a "curried" selector creator function with the specified memoization function and options for customizing memoization behavior. - * - * @param createSelectorCreatorArgs - Same parameters as {@linkcode createSelectorCreator createSelectorCreator} - * @returns A customized `createCurriedSelector` function. - * - * @example - * ```ts - * import { createCurriedSelectorCreator, createSelector, weakMapMemoize } from 'reselect' - * import { useSelector } from 'react-redux' - * - * interface RootState { - * todos: { id: number; completed: boolean }[] - * alerts: { id: number; read: boolean }[] - * } - * - * const state: RootState = { - * todos: [ - * { id: 0, completed: false }, - * { id: 1, completed: true } - * ], - * alerts: [ - * { id: 0, read: false }, - * { id: 1, read: true } - * ] - * } - * - * const parametricSelector = createSelector( - * [(state: RootState) => state.todos, (state: RootState, id: number) => id], - * (todos, id) => todos.filter(todo => todo.id === id), - * { memoize: weakMapMemoize, argsMemoize: weakMapMemoize } - * ) - * - * const createCurriedSelectorWeakMap = createCurriedSelectorCreator({ memoize: weakMapMemoize, argsMemoize: weakMapMemoize }) - * - * const curriedSelector = createCurriedSelectorWeakMap( - * [(state: RootState) => state.todos, (state: RootState, id: number) => id], - * (todos, id) => todos.filter(todo => todo.id === id) - * ) - * - * // This: - * parametricSelector(state, 0) - * - * // Is the same as this: - * curriedSelector(0)(state) - * - * // Inside your component you can replace this: - * const selectTodo = useSelector(state => parametricSelector(state, id)) - * - * // With this: - * const selectTodo = useSelector(curriedSelector(id)) - * ``` - * - * @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createCurriedSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). - * @template ArgsMemoizeFunction - The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createCurriedSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used. - * - * @since 5.0.0 - * @public - * @experimental - */ -export function createCurriedSelectorCreator< - MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, - ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize ->(...createSelectorCreatorArgs: Parameters) { - const createSelector = createSelectorCreator(...createSelectorCreatorArgs) - - const createCurriedSelector = ( - ...createSelectorArgs: Parameters< - CreateSelectorFunction - > - ) => { - // @ts-ignore - const selector = createSelector.apply(null, createSelectorArgs) - const curriedSelector = selector.argsMemoize( - (...params: DropFirstParameter) => { - return selector.argsMemoize((state: Parameters[0]) => { - return selector(state, ...params) - }) - } - ) - return Object.assign(curriedSelector, selector) as CurriedOutputSelector - } - return createCurriedSelector as unknown as CreateCurriedSelector< - MemoizeFunction, - ArgsMemoizeFunction - > -} - -/** - * Accepts one or more "input selectors" (either as separate arguments or a single array), - * a single "result function" / "combiner", and an optional options object, and - * generates a memoized "curried" selector function. - * - * @example - * ```ts - * import { createCurriedSelector, createSelector } from 'reselect' - * import { useSelector } from 'react-redux' - * - * interface RootState { - * todos: { id: number; completed: boolean }[] - * alerts: { id: number; read: boolean }[] - * } - * - * const state: RootState = { - * todos: [ - * { id: 0, completed: false }, - * { id: 1, completed: true } - * ], - * alerts: [ - * { id: 0, read: false }, - * { id: 1, read: true } - * ] - * } - * - * const parametricSelector = createSelector( - * [(state: RootState) => state.todos, (state: RootState, id: number) => id], - * (todos, id) => todos.filter(todo => todo.id === id) - * ) - * - * // This: - * parametricSelector(state, 0) - * - * const curriedSelector = createCurriedSelector( - * [(state: RootState) => state.todos, (state: RootState, id: number) => id], - * (todos, id) => todos.filter(todo => todo.id === id) - * ) - * - * // Is the same as this: - * curriedSelector(0)(state) - * - * // Inside your component you can replace this: - * const selectTodo = useSelector(state => parametricSelector(state, id)) - * - * // With this: - * const selectTodo = useSelector(curriedSelector(id)) - * ``` - * - * @since 5.0.0 - * @public - * @experimental - */ -export const createCurriedSelector = - /* #__PURE__ */ createCurriedSelectorCreator(defaultMemoize) diff --git a/src/index.ts b/src/index.ts index dde6f5ae4..6a85cb051 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,4 @@ export { autotrackMemoize as unstable_autotrackMemoize } from './autotrackMemoize/autotrackMemoize' -export { - createCurriedSelector, - createCurriedSelectorCreator -} from './createCurriedSelectorCreator' -export type { CreateCurriedSelector } from './createCurriedSelectorCreator' export { createSelector, createSelectorCreator, @@ -20,7 +15,6 @@ export type { DefaultMemoizeOptions } from './defaultMemoize' export type { Combiner, CreateSelectorOptions, - CurriedOutputSelector, DefaultMemoizeFields, EqualityFn, ExtractMemoizerFields, diff --git a/src/types.ts b/src/types.ts index 3ec3f0404..8a8dca4db 100644 --- a/src/types.ts +++ b/src/types.ts @@ -93,19 +93,18 @@ export interface CreateSelectorOptions< * ```ts * import { createSelector, weakMapMemoize } from 'reselect' * - * const selectTodosById = createSelector( + * const selectItemsByCategory = createSelector( * [ - * (state: RootState) => state.todos, - * (state: RootState, id: number) => id + * (state: RootState) => state.items, + * (state: RootState, category: string) => category * ], - * (todos, id) => todos.filter(todo => todo.id === id), + * (items, category) => items.filter(item => item.category === category), * { memoize: weakMapMemoize } * ) * ``` * * @since 5.0.0 */ - // If `memoize` is not provided inside the options object, fallback to `MemoizeFunction` which is the original memoize function passed into `createSelectorCreator`. memoize?: FallbackIfNever /** @@ -121,12 +120,12 @@ export interface CreateSelectorOptions< * ```ts * import { createSelector, weakMapMemoize } from 'reselect' * - * const selectTodosById = createSelector( + * const selectItemsByCategory = createSelector( * [ - * (state: RootState) => state.todos, - * (state: RootState, id: number) => id + * (state: RootState) => state.items, + * (state: RootState, category: string) => category * ], - * (todos, id) => todos.filter(todo => todo.id === id), + * (items, category) => items.filter(item => item.category === category), * { argsMemoize: weakMapMemoize } * ) * ``` @@ -135,9 +134,6 @@ export interface CreateSelectorOptions< * * @since 5.0.0 */ - // If `argsMemoize` is not provided inside the options object, - // fallback to `ArgsMemoizeFunction` which is the original `argsMemoize` function passed into `createSelectorCreator`. - // If none was passed originally to `createSelectorCreator`, it should fallback to `defaultMemoize`. argsMemoize?: FallbackIfNever< OverrideArgsMemoizeFunction, ArgsMemoizeFunction @@ -149,7 +145,6 @@ export interface CreateSelectorOptions< * * @since 5.0.0 */ - // Should dynamically change to the options argument of `memoize`. memoizeOptions?: OverrideMemoizeOptions< MemoizeFunction, OverrideMemoizeFunction @@ -408,32 +403,6 @@ export type DefaultMemoizeFields = { clearCache: () => void } -/** - * Represents a curried {@linkcode OutputSelector OutputSelector}. - * - * @template InputSelectors - The type of the input selectors. - * @template Result - The type of the result returned by the `resultFunc`. - * @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createCurriedSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). - * @template ArgsMemoizeFunction - The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createCurriedSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used. - * - * @public - */ -export type CurriedOutputSelector< - InputSelectors extends SelectorArray = SelectorArray, - Result = unknown, - MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, - ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize -> = Curried< - OutputSelector -> & - ExtractMemoizerFields & - OutputSelectorFields< - InputSelectors, - Result, - MemoizeFunction, - ArgsMemoizeFunction - > - /* * ----------------------------------------------------------------------------- * ----------------------------------------------------------------------------- @@ -567,17 +536,6 @@ export type AnyNonNullishValue = NonNullable */ export type InterruptRecursion = AnyNonNullishValue -/** - * Create a curried version of a function. - * - * @template FunctionType The original function type. - * - * @internal - */ -export type Curried any> = ( - ...params: DropFirstParameter -) => (state: Parameters[0]) => ReturnType - /* * ----------------------------------------------------------------------------- * ----------------------------------------------------------------------------- diff --git a/test/autotrackMemoize.spec.ts b/test/autotrackMemoize.spec.ts index bdabb689b..726f18d16 100644 --- a/test/autotrackMemoize.spec.ts +++ b/test/autotrackMemoize.spec.ts @@ -35,7 +35,6 @@ describe('Basic selector behavior with autotrack', () => { (state: StateA) => state.a, a => a ) - selector.memoizedResultFunc.clearCache const firstState = { a: 1 } const firstStateNewPointer = { a: 1 } const secondState = { a: 2 } diff --git a/test/benchmarks/createCurriedSelector.bench.ts b/test/benchmarks/createCurriedSelector.bench.ts deleted file mode 100644 index aab34bea6..000000000 --- a/test/benchmarks/createCurriedSelector.bench.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { createCurriedSelector, createSelector } from 'reselect' -import type { Options } from 'tinybench' -import { bench } from 'vitest' -import type { RootState } from '../testUtils' -import { setFunctionNames, setupStore } from '../testUtils' - -describe('curriedSelector vs parametric selector', () => { - const commonOptions: Options = { - iterations: 10, - time: 0 - } - const store = setupStore() - const state = store.getState() - const parametricSelector = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.find(todo => todo.id === id) - ) - const curriedSelector = createCurriedSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.find(todo => todo.id === id) - ) - setFunctionNames({ parametricSelector, curriedSelector }) - bench( - parametricSelector, - () => { - parametricSelector(state, 0) - }, - { - ...commonOptions, - setup: (task, mode) => { - if (mode === 'warmup') return - parametricSelector.clearCache() - parametricSelector.resetRecomputations() - parametricSelector.memoizedResultFunc.clearCache() - } - } - ) - bench( - curriedSelector, - () => { - curriedSelector(0)(state) - }, - { - ...commonOptions, - setup: (task, mode) => { - if (mode === 'warmup') return - curriedSelector.clearCache() - curriedSelector.resetRecomputations() - curriedSelector.memoizedResultFunc.clearCache() - } - } - ) -}) diff --git a/type-tests/createCurriedSrlector.test-d.ts b/type-tests/createCurriedSrlector.test-d.ts deleted file mode 100644 index 6611e22c1..000000000 --- a/type-tests/createCurriedSrlector.test-d.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { createCurriedSelector, createSelector, defaultMemoize } from 'reselect' -import { describe, expectTypeOf, test } from 'vitest' - -interface RootState { - todos: { - id: number - completed: boolean - }[] -} - -const state: RootState = { - todos: [ - { id: 0, completed: false }, - { id: 1, completed: false } - ] -} - -describe('curried selector', () => { - test('curried selector fields args as array', () => { - const curriedSelector = createCurriedSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos[id] - ) - const parametricSelector = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos[id] - ) - expectTypeOf(curriedSelector.argsMemoize).toEqualTypeOf(defaultMemoize) - expectTypeOf(curriedSelector.memoize).toEqualTypeOf(defaultMemoize) - expectTypeOf(curriedSelector.clearCache).toEqualTypeOf( - parametricSelector.clearCache - ) - expectTypeOf(curriedSelector.dependencies).toEqualTypeOf( - parametricSelector.dependencies - ) - expectTypeOf(curriedSelector.lastResult).toEqualTypeOf( - parametricSelector.lastResult - ) - expectTypeOf(curriedSelector.lastResult).returns.toEqualTypeOf( - parametricSelector.lastResult() - ) - expectTypeOf(curriedSelector.memoizedResultFunc).toEqualTypeOf( - parametricSelector.memoizedResultFunc - ) - expectTypeOf(curriedSelector.recomputations).toEqualTypeOf( - parametricSelector.recomputations - ) - expectTypeOf(curriedSelector.resetRecomputations).toEqualTypeOf( - parametricSelector.resetRecomputations - ) - expectTypeOf(curriedSelector.resultFunc).toEqualTypeOf( - parametricSelector.resultFunc - ) - expectTypeOf(curriedSelector.memoizedResultFunc.clearCache).toEqualTypeOf( - parametricSelector.memoizedResultFunc.clearCache - ) - expectTypeOf(curriedSelector(0)(state)).toEqualTypeOf( - parametricSelector(state, 0) - ) - }) - - test('curried selector fields separate inline args', () => { - const curriedSelector = createCurriedSelector( - (state: RootState) => state.todos, - (state: RootState, id: number) => id, - (todos, id) => todos[id] - ) - const parametricSelector = createSelector( - (state: RootState) => state.todos, - (state: RootState, id: number) => id, - (todos, id) => todos[id] - ) - expectTypeOf(curriedSelector.argsMemoize).toEqualTypeOf(defaultMemoize) - expectTypeOf(curriedSelector.memoize).toEqualTypeOf(defaultMemoize) - expectTypeOf(curriedSelector.clearCache).toEqualTypeOf( - parametricSelector.clearCache - ) - expectTypeOf(curriedSelector.dependencies).toEqualTypeOf( - parametricSelector.dependencies - ) - expectTypeOf(curriedSelector.lastResult).toEqualTypeOf( - parametricSelector.lastResult - ) - expectTypeOf(curriedSelector.lastResult).returns.toEqualTypeOf( - parametricSelector.lastResult() - ) - expectTypeOf(curriedSelector.memoizedResultFunc).toEqualTypeOf( - parametricSelector.memoizedResultFunc - ) - expectTypeOf(curriedSelector.recomputations).toEqualTypeOf( - parametricSelector.recomputations - ) - expectTypeOf(curriedSelector.resetRecomputations).toEqualTypeOf( - parametricSelector.resetRecomputations - ) - expectTypeOf(curriedSelector.resultFunc).toEqualTypeOf( - parametricSelector.resultFunc - ) - expectTypeOf(curriedSelector.memoizedResultFunc.clearCache).toEqualTypeOf( - parametricSelector.memoizedResultFunc.clearCache - ) - expectTypeOf(curriedSelector(0)(state)).toEqualTypeOf( - parametricSelector(state, 0) - ) - }) -}) From de37737dc925dda6fe61d134e56548242e83b839 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sat, 18 Nov 2023 14:56:03 -0600 Subject: [PATCH 31/50] Slightly Modify `createParametricSelectorHook` in docs --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dabe9f9d7..8e8ff2129 100644 --- a/README.md +++ b/README.md @@ -1640,10 +1640,13 @@ Another thing you can do if you are using [React-Redux] is create a custom hook import type { GetParamsFromSelectors, Selector } from 'reselect' import { useSelector } from 'react-redux' -export const createParametricSelectorHook = ( - selector: S +export const createParametricSelectorHook = < + Result, + Params extends readonly unknown[] +>( + selector: (state: RootState, ...params: Params) => Result ) => { - return (...args: GetParamsFromSelectors<[S]>) => { + return (...args: Params) => { return useSelector(state => selector(state, ...args)) } } From 38e7a40a939ed766f292e73658cf6f92299c4c67 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sat, 18 Nov 2023 14:56:45 -0600 Subject: [PATCH 32/50] Fix installation guide in docs --- README.md | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 8e8ff2129..36ccd92c7 100644 --- a/README.md +++ b/README.md @@ -24,27 +24,17 @@ import { createSelector } from '@reduxjs/toolkit' For standalone usage, install the `reselect` package: -#### Using `npm` - ```bash +# NPM npm install reselect -``` - -#### Using `yarn` -```bash +# Yarn yarn add reselect -``` -#### Using `bun` - -```bash +# Bun bun add reselect -``` -#### Using `pnpm` - -```bash +# PNPM pnpm add reselect ``` From 3845404198f511dd71c05ddcdee8eff3fe05fde8 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sat, 18 Nov 2023 16:17:26 -0600 Subject: [PATCH 33/50] Modify example for `createStructuredSelector` in docs. --- README.md | 60 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 36ccd92c7..9343fc0e2 100644 --- a/README.md +++ b/README.md @@ -488,27 +488,21 @@ A memoized structured selector. import { createSelector, createStructuredSelector } from 'reselect' interface RootState { - todos: { id: number; completed: boolean }[] + todos: { + id: number + completed: boolean + title: string + description: string + }[] alerts: { id: number; read: boolean }[] } -const state: RootState = { - todos: [ - { id: 0, completed: false }, - { id: 1, completed: true } - ], - alerts: [ - { id: 0, read: false }, - { id: 1, read: true } - ] -} - // This: const structuredSelector = createStructuredSelector( { - allTodos: (state: RootState) => state.todos, - allAlerts: (state: RootState) => state.alerts, - selectedTodo: (state: RootState, id: number) => state.todos[id] + todos: (state: RootState) => state.todos, + alerts: (state: RootState) => state.alerts, + todoById: (state: RootState, id: number) => state.todos[id] }, createSelector ) @@ -520,16 +514,44 @@ const selector = createSelector( (state: RootState) => state.alerts, (state: RootState, id: number) => state.todos[id] ], - (allTodos, allAlerts, selectedTodo) => { + (todos, alerts, todoById) => { return { - allTodos, - allAlerts, - selectedTodo + todos, + alerts, + todoById } } ) ``` +In your component: + +```tsx +interface Props { + id: number +} + +const MyComponent: FC = ({ id }) => { + const { todos, alerts, todoById } = useSelector(state => + structuredSelector(state, id) + ) + + return ( +
+ Next to do is: +

{todoById.title}

+

Description: {todoById.description}

+
    +

    All other to dos:

    + {todos.map(todo => ( +
  • {todo.title}
  • + ))} +
+
+ ) +} +``` + ##### Simple Use Case ```ts From 4458991b39c0a89a429f9b7002aee4f71b67ee07 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sat, 18 Nov 2023 17:34:22 -0600 Subject: [PATCH 34/50] Add explanation underneath "How Does Reselect Work" in docs. --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 9343fc0e2..f56df3d21 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,19 @@ const outputSelector = createSelector( ## How Does Reselect Work? +Reselect, at its core, is a library for creating memoized selectors in JavaScript applications. Its primary role is to efficiently compute derived data based on provided inputs. A key aspect of Reselect's internal mechanism is how it orchestrates the flow of arguments from the final selector to its constituent [input selectors]. + +```ts +const finalSelector = (...args) => { + const extractedValues = inputSelectors.map(inputSelector => + inputSelector(...args) + ) + return resultFunc(...extractedValues) +} +``` + +In this pattern, the `finalSelector` is composed of several [input selectors], **all receiving the same arguments as the final selector**. Each input selector processes its part of the data, and the results are then combined and further processed by the [result function]. Understanding this argument flow is crucial for appreciating how Reselect optimizes data computation and minimizes unnecessary recalculations. + ### Cascading Memoization @@ -576,6 +589,8 @@ const result = structuredSelector({ a: 1, b: 2 }) // will produce { x: 1, y: 2 } ### Memoization Functions +Reselect comes with a selection of memoization functions, each uniquely designed to address different scenarios and performance requirements. By effectively leveraging these functions, you can significantly enhance the efficiency and responsiveness of your applications. + #### defaultMemoize(func, equalityCheckOrOptions = defaultEqualityCheck) From 4bb0655491d3cb245cb4c5b5e450427aae2d0c1b Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sat, 18 Nov 2023 17:54:03 -0600 Subject: [PATCH 35/50] Modify JSDocs for `createStructuredSelector` to match the docs. --- README.md | 4 +- src/createStructuredSelector.ts | 116 +++++++++++++++----------------- 2 files changed, 58 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index f56df3d21..1653fb01b 100644 --- a/README.md +++ b/README.md @@ -466,7 +466,7 @@ const selector = customSelectorCreator( -### createStructuredSelector({ inputSelectors }, selectorCreator = createSelector) +### createStructuredSelector({ inputSelectorsObject }, selectorCreator = createSelector) Description @@ -476,7 +476,7 @@ A convenience function that simplifies returning an object made up of selector r | Name | Description | | :----------------- | :--------------------------------------------------------------------- | -| `selectorMap` | A key value pair consisting of input selectors. | +| `inputSelectorsObject` | A key value pair consisting of input selectors. | | `selectorCreator?` | A custom selector creator function. It defaults to [`createSelector`]. | Returns diff --git a/src/createStructuredSelector.ts b/src/createStructuredSelector.ts index be926cc2a..4a6dc664c 100644 --- a/src/createStructuredSelector.ts +++ b/src/createStructuredSelector.ts @@ -67,11 +67,10 @@ interface SelectorsObject { */ export interface StructuredSelectorCreator { /** - * A convenience function for a common pattern that arises when using Reselect. - * The selector passed to a `connect` decorator often just takes the - * values of its input selectors and maps them to keys in an object. + * A convenience function that simplifies returning an object + * made up of selector results. * - * @param selectorMap - A key value pair consisting of input selectors. + * @param inputSelectorsObject - A key value pair consisting of input selectors. * @param selectorCreator - A custom selector creator function. It defaults to `createSelector`. * @returns A memoized structured selector. * @@ -81,27 +80,21 @@ export interface StructuredSelectorCreator { * import { createSelector, createStructuredSelector } from 'reselect' * * interface RootState { - * todos: { id: number; completed: boolean }[] + * todos: { + * id: number + * completed: boolean + * title: string + * description: string + * }[] * alerts: { id: number; read: boolean }[] * } * - * const state: RootState = { - * todos: [ - * { id: 0, completed: false }, - * { id: 1, completed: true } - * ], - * alerts: [ - * { id: 0, read: false }, - * { id: 1, read: true } - * ] - * } - * * // This: * const structuredSelector = createStructuredSelector( * { - * allTodos: (state: RootState) => state.todos, - * allAlerts: (state: RootState) => state.alerts, - * selectedTodo: (state: RootState, id: number) => state.todos[id] + * todos: (state: RootState) => state.todos, + * alerts: (state: RootState) => state.alerts, + * todoById: (state: RootState, id: number) => state.todos[id] * }, * createSelector * ) @@ -113,11 +106,11 @@ export interface StructuredSelectorCreator { * (state: RootState) => state.alerts, * (state: RootState, id: number) => state.todos[id] * ], - * (allTodos, allAlerts, selectedTodo) => { + * (todos, alerts, todoById) => { * return { - * allTodos, - * allAlerts, - * selectedTodo + * todos, + * alerts, + * todoById * } * } * ) @@ -150,7 +143,7 @@ export interface StructuredSelectorCreator { MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize >( - selectorMap: InputSelectorsObject, + inputSelectorsObject: InputSelectorsObject, selectorCreator?: CreateSelectorFunction< MemoizeFunction, ArgsMemoizeFunction @@ -162,49 +155,52 @@ export interface StructuredSelectorCreator { ArgsMemoizeFunction > & InterruptRecursion - // TODO: Do we need this? - /** - * Second overload - */ - // < - // State, - // Result = State, - // MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, - // ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize - // >( - // selectors: { - // [Key in keyof State]: Selector - // }, - // selectorCreator?: CreateSelectorFunction< - // MemoizeFunction, - // ArgsMemoizeFunction - // > - // ): OutputSelector< - // readonly Selector[], - // Result, - // MemoizeFunction, - // ArgsMemoizeFunction - // > } -// Manual definition of state and output arguments /** - * A convenience function for a common pattern that arises when using Reselect. - * The selector passed to a `connect` decorator often just takes the values of its input selectors - * and maps them to keys in an object. + * A convenience function that simplifies returning an object + * made up of selector results. * * @example - * Simple Use Case + * Modern Use Case * ```ts - * const selectA = state => state.a - * const selectB = state => state.b + * import { createSelector, createStructuredSelector } from 'reselect' + * + * interface RootState { + * todos: { + * id: number + * completed: boolean + * title: string + * description: string + * }[] + * alerts: { id: number; read: boolean }[] + * } + * + * // This: + * const structuredSelector = createStructuredSelector( + * { + * todos: (state: RootState) => state.todos, + * alerts: (state: RootState) => state.alerts, + * todoById: (state: RootState, id: number) => state.todos[id] + * }, + * createSelector + * ) * - * // The result function in the following selector - * // is simply building an object from the input selectors - * const structuredSelector = createSelector(selectA, selectB, (a, b) => ({ - * a, - * b - * })) + * // Is essentially the same as this: + * const selector = createSelector( + * [ + * (state: RootState) => state.todos, + * (state: RootState) => state.alerts, + * (state: RootState, id: number) => state.todos[id] + * ], + * (todos, alerts, todoById) => { + * return { + * todos, + * alerts, + * todoById + * } + * } + * ) * ``` * * @see {@link https://github.com/reduxjs/reselect#createstructuredselectorinputselectors-selectorcreator--createselector createStructuredSelector} From 3871ae4252cb384e4997e0e698ecf5da5efabc0f Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sat, 18 Nov 2023 19:05:58 -0600 Subject: [PATCH 36/50] Update `createStructuredSelector` link in docs --- README.md | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1653fb01b..775100972 100644 --- a/README.md +++ b/README.md @@ -441,7 +441,7 @@ const selectSum = createDeepEqualSelector( ) ``` -###### Use memoize function from `Lodash` for an unbounded cache +###### Use memoize function from Lodash for an unbounded cache ```js import { createSelectorCreator } from 'reselect' @@ -474,10 +474,10 @@ A convenience function that simplifies returning an object made up of selector r Parameters -| Name | Description | -| :----------------- | :--------------------------------------------------------------------- | -| `inputSelectorsObject` | A key value pair consisting of input selectors. | -| `selectorCreator?` | A custom selector creator function. It defaults to [`createSelector`]. | +| Name | Description | +| :--------------------- | :--------------------------------------------------------------------- | +| `inputSelectorsObject` | A key value pair consisting of input selectors. | +| `selectorCreator?` | A custom selector creator function. It defaults to [`createSelector`]. | Returns @@ -1632,6 +1632,31 @@ const selectTodoByIdCurried = currySelector( ) ``` +Or for reusability you can do this: + +```ts +import type { defaultMemoize, SelectorArray, UnknownMemoizer } from 'reselect' +import { createSelector } from 'reselect' + +export const createCurriedSelector = < + InputSelectors extends SelectorArray, + Result, + OverrideMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + OverrideArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize +>( + ...args: Parameters< + typeof createSelector< + InputSelectors, + Result, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > + > +) => { + return currySelector(createSelector(...args)) +} +``` + This: ```ts @@ -1664,7 +1689,6 @@ const todoById = useSelector(selectTodoByIdCurried(id)) Another thing you can do if you are using [React-Redux] is create a custom hook factory function: ```ts -import type { GetParamsFromSelectors, Selector } from 'reselect' import { useSelector } from 'react-redux' export const createParametricSelectorHook = < @@ -1678,7 +1702,7 @@ export const createParametricSelectorHook = < } } -const useSelectTodo = createParametricSelectorHook(parametricSelector) +const useSelectTodo = createParametricSelectorHook(selectTodoById) ``` And then inside your component: @@ -1865,7 +1889,7 @@ Originally inspired by getters in [NuclearJS](https://github.com/optimizely/nucl [`defaultMemoize`]: #defaultmemoizefunc-equalitycheckoroptions--defaultequalitycheck 'defaultMemoize' [`weakMapMemoize`]: #weakmapmemoizefunc---since-500 'weakMapMemoize' [`unstable_autotrackMemoize`]: #unstable_autotrackmemoizefunc---since-500 'unstable_autotrackMemoize' -[`createStructuredSelector`]: #createstructuredselector-inputselectors--selectorcreator--createselector 'createStructuredSelector' +[`createStructuredSelector`]: #createstructuredselector-inputSelectorsObject--selectorcreator--createselector 'createStructuredSelector'
From 04d8241b737b214c9d8b539720f9adda8c089ef3 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sat, 18 Nov 2023 19:07:24 -0600 Subject: [PATCH 37/50] Update `examples.test.ts` to match the docs. --- test/examples.test.ts | 73 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/test/examples.test.ts b/test/examples.test.ts index 973435521..f487097e7 100644 --- a/test/examples.test.ts +++ b/test/examples.test.ts @@ -46,7 +46,8 @@ test('identity', () => { }) const nonMemoizedSelector = createNonMemoizedSelector( [(state: RootState) => state.todos], - todos => todos.filter(todo => todo.completed === true) + todos => todos.filter(todo => todo.completed === true), + { inputStabilityCheck: 'never' } ) nonMemoizedSelector(store.getState()) @@ -56,7 +57,7 @@ test('identity', () => { expect(nonMemoizedSelector.recomputations()).toBe(3) }) -test('Top Level Selectors', () => { +test.todo('Top Level Selectors', () => { type TopLevelSelectors = { [K in keyof State as K extends string ? `select${Capitalize}` @@ -70,7 +71,7 @@ test('Top Level Selectors', () => { } }) -test('Find Fastest Selector', () => { +test.todo('Find Fastest Selector', () => { const store = setupStore() const selectTodoIds = createSelector( [(state: RootState) => state.todos], @@ -120,10 +121,6 @@ test('Find Fastest Selector', () => { } return { results, fastest } as const } - - expect( - findFastestSelector(selectTodoIds, store.getState()).fastest.name - ).toBe(weakMapMemoize.name) }) test('TypedCreateSelector', () => { @@ -159,3 +156,65 @@ test('TypedCreateSelector', () => { (todos, id) => todos.find(todo => todo.id === id)?.completed ) }) + +test('createCurriedSelector copy paste pattern', () => { + const state = store.getState() + const currySelector = < + State, + Result, + Params extends readonly any[], + AdditionalFields + >( + selector: ((state: State, ...args: Params) => Result) & AdditionalFields + ) => { + const curriedSelector = (...args: Params) => { + return (state: State) => { + return selector(state, ...args) + } + } + return Object.assign(curriedSelector, selector) + } + + const createCurriedSelector = < + InputSelectors extends SelectorArray, + Result, + OverrideMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + OverrideArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize + >( + ...args: Parameters< + typeof createSelector< + InputSelectors, + Result, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > + > + ) => { + return currySelector(createSelector(...args)) + } + const selectTodoById = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id) + ) + const selectTodoByIdCurried = createCurriedSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + (todos, id) => todos.find(todo => todo.id === id) + ) + expect(selectTodoById(state, 0)).toStrictEqual( + selectTodoByIdCurried(0)(state) + ) + expect(selectTodoById.argsMemoize).toBe(selectTodoByIdCurried.argsMemoize) + expect(selectTodoById.lastResult()).toBeDefined() + expect(selectTodoByIdCurried.lastResult()).toBeDefined() + expect(selectTodoById.lastResult()).toBe(selectTodoByIdCurried.lastResult()) + expect(selectTodoById.memoize).toBe(selectTodoByIdCurried.memoize) + expect(selectTodoById.memoizedResultFunc(state.todos, 0)).toBe( + selectTodoByIdCurried.memoizedResultFunc(state.todos, 0) + ) + expect(selectTodoById.recomputations()).toBe( + selectTodoByIdCurried.recomputations() + ) + expect(selectTodoById.resultFunc(state.todos, 0)).toBe( + selectTodoByIdCurried.resultFunc(state.todos, 0) + ) +}) From 55ca12e3757657e1f5f94bb8ddb031ae29fb7906 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sat, 18 Nov 2023 19:16:09 -0600 Subject: [PATCH 38/50] Add `dependencyRecomputations` and `resetDependencyRecomputations` to tests --- test/reselect.spec.ts | 111 +++++++++++++++++++++++++++--------------- test/testUtils.ts | 14 ++---- 2 files changed, 75 insertions(+), 50 deletions(-) diff --git a/test/reselect.spec.ts b/test/reselect.spec.ts index 599833640..e9576fa1c 100644 --- a/test/reselect.spec.ts +++ b/test/reselect.spec.ts @@ -481,7 +481,9 @@ describe('argsMemoize and memoize', () => { 'lastResult' in selector && 'dependencies' in selector && 'recomputations' in selector && + 'dependencyRecomputations' in selector && 'resetRecomputations' in selector && + 'resetDependencyRecomputations' in selector && 'memoize' in selector && 'argsMemoize' in selector && typeof selector.resultFunc === 'function' && @@ -489,7 +491,9 @@ describe('argsMemoize and memoize', () => { typeof selector.lastResult === 'function' && Array.isArray(selector.dependencies) && typeof selector.recomputations === 'function' && + typeof selector.dependencyRecomputations === 'function' && typeof selector.resetRecomputations === 'function' && + typeof selector.resetDependencyRecomputations === 'function' && typeof selector.memoize === 'function' && typeof selector.argsMemoize === 'function' && selector.dependencies.length >= 1 && @@ -519,7 +523,9 @@ describe('argsMemoize and memoize', () => { expect(selectorDefault.lastResult).to.be.a('function') expect(selectorDefault.dependencies).to.be.an('array').that.is.not.empty expect(selectorDefault.recomputations).to.be.a('function') + expect(selectorDefault.dependencyRecomputations).to.be.a('function') expect(selectorDefault.resetRecomputations).to.be.a('function') + expect(selectorDefault.resetDependencyRecomputations).to.be.a('function') expect(selectorDefault.memoize).to.be.a('function') expect(selectorDefault.argsMemoize).to.be.a('function') expect(selectorDefault.clearCache).to.be.a('function') @@ -529,7 +535,9 @@ describe('argsMemoize and memoize', () => { expect(selectorAutotrack.recomputations()).toBe(0) expect(selectorDefault(state)).toStrictEqual(selectorAutotrack(state)) expect(selectorDefault.recomputations()).toBe(1) + expect(selectorDefault.dependencyRecomputations()).toBe(1) expect(selectorAutotrack.recomputations()).toBe(1) + expect(selectorAutotrack.dependencyRecomputations()).toBe(1) // flipping completed flag does not cause the autotrack memoizer to re-run. store.dispatch(toggleCompleted(0)) selectorDefault(store.getState()) @@ -542,14 +550,18 @@ describe('argsMemoize and memoize', () => { const defaultSelectorLastResult2 = selectorDefault.lastResult() const autotrackSelectorLastResult2 = selectorAutotrack.lastResult() expect(selectorDefault.recomputations()).toBe(3) + expect(selectorDefault.dependencyRecomputations()).toBe(3) expect(selectorAutotrack.recomputations()).toBe(1) + expect(selectorAutotrack.dependencyRecomputations()).toBe(3) for (let i = 0; i < 10; i++) { store.dispatch(toggleCompleted(0)) selectorDefault(store.getState()) selectorAutotrack(store.getState()) } expect(selectorDefault.recomputations()).toBe(13) + expect(selectorDefault.dependencyRecomputations()).toBe(13) expect(selectorAutotrack.recomputations()).toBe(1) + expect(selectorAutotrack.dependencyRecomputations()).toBe(13) expect(autotrackSelectorLastResult1).toBe(autotrackSelectorLastResult2) expect(defaultSelectorLastResult1).not.toBe(defaultSelectorLastResult2) // Default memoize does not preserve referential equality but autotrack does. expect(defaultSelectorLastResult1).toStrictEqual(defaultSelectorLastResult2) @@ -561,6 +573,7 @@ describe('argsMemoize and memoize', () => { ) selectorAutotrack(store.getState()) expect(selectorAutotrack.recomputations()).toBe(2) + expect(selectorAutotrack.dependencyRecomputations()).toBe(14) }) localTest('passing argsMemoize directly to createSelector', ({ store }) => { @@ -583,6 +596,8 @@ describe('argsMemoize and memoize', () => { ) expect(selectorDefault.recomputations()).toBe(1) expect(selectorAutotrack.recomputations()).toBe(1) + expect(selectorDefault.dependencyRecomputations()).toBe(1) + expect(selectorAutotrack.dependencyRecomputations()).toBe(1) selectorDefault(store.getState()) selectorAutotrack(store.getState()) // toggling the completed flag should force the default memoizer to recalculate but not autotrack. @@ -602,6 +617,8 @@ describe('argsMemoize and memoize', () => { store.dispatch(toggleCompleted(2)) expect(selectorDefault.recomputations()).toBe(4) expect(selectorAutotrack.recomputations()).toBe(1) + expect(selectorDefault.dependencyRecomputations()).toBe(4) + expect(selectorAutotrack.dependencyRecomputations()).toBe(4) selectorDefault(store.getState()) selectorAutotrack(store.getState()) store.dispatch(toggleCompleted(0)) @@ -617,6 +634,8 @@ describe('argsMemoize and memoize', () => { const autotrackSelectorLastResult2 = selectorAutotrack.lastResult() expect(selectorDefault.recomputations()).toBe(6) expect(selectorAutotrack.recomputations()).toBe(1) + expect(selectorDefault.dependencyRecomputations()).toBe(6) + expect(selectorAutotrack.dependencyRecomputations()).toBe(7) expect(autotrackSelectorLastResult1).toBe(autotrackSelectorLastResult2) expect(defaultSelectorLastResult1).not.toBe(defaultSelectorLastResult2) expect(defaultSelectorLastResult1).toStrictEqual(defaultSelectorLastResult2) @@ -630,6 +649,8 @@ describe('argsMemoize and memoize', () => { } expect(selectorAutotrack.recomputations()).toBe(1) expect(selectorDefault.recomputations()).toBe(16) + expect(selectorAutotrack.dependencyRecomputations()).toBe(17) + expect(selectorDefault.dependencyRecomputations()).toBe(16) // original options untouched. const selectorOriginal = createSelector( [(state: RootState) => state.todos], @@ -662,6 +683,8 @@ describe('argsMemoize and memoize', () => { } expect(selectorOverrideArgsMemoize.recomputations()).toBe(1) expect(selectorOriginal.recomputations()).toBe(11) + expect(selectorOverrideArgsMemoize.dependencyRecomputations()).toBe(1) + expect(selectorOriginal.dependencyRecomputations()).toBe(11) const selectorDefaultParametric = createSelector( [(state: RootState, id: number) => id, (state: RootState) => state.todos], (id, todos) => todos.filter(todo => todo.id === id) @@ -669,11 +692,14 @@ describe('argsMemoize and memoize', () => { selectorDefaultParametric(store.getState(), 1) selectorDefaultParametric(store.getState(), 1) expect(selectorDefaultParametric.recomputations()).toBe(1) + expect(selectorDefaultParametric.dependencyRecomputations()).toBe(1) selectorDefaultParametric(store.getState(), 2) selectorDefaultParametric(store.getState(), 1) expect(selectorDefaultParametric.recomputations()).toBe(3) + expect(selectorDefaultParametric.dependencyRecomputations()).toBe(3) selectorDefaultParametric(store.getState(), 2) expect(selectorDefaultParametric.recomputations()).toBe(4) + expect(selectorDefaultParametric.dependencyRecomputations()).toBe(4) const selectorDefaultParametricArgsWeakMap = createSelector( [(state: RootState, id: number) => id, (state: RootState) => state.todos], (id, todos) => todos.filter(todo => todo.id === id), @@ -687,14 +713,23 @@ describe('argsMemoize and memoize', () => { selectorDefaultParametricArgsWeakMap(store.getState(), 1) selectorDefaultParametricArgsWeakMap(store.getState(), 1) expect(selectorDefaultParametricArgsWeakMap.recomputations()).toBe(1) + expect( + selectorDefaultParametricArgsWeakMap.dependencyRecomputations() + ).toBe(1) selectorDefaultParametricArgsWeakMap(store.getState(), 2) selectorDefaultParametricArgsWeakMap(store.getState(), 1) expect(selectorDefaultParametricArgsWeakMap.recomputations()).toBe(2) + expect( + selectorDefaultParametricArgsWeakMap.dependencyRecomputations() + ).toBe(2) selectorDefaultParametricArgsWeakMap(store.getState(), 2) // If we call the selector with 1, then 2, then 1 and back to 2 again, // `defaultMemoize` will recompute a total of 4 times, // but weakMapMemoize will recompute only twice. expect(selectorDefaultParametricArgsWeakMap.recomputations()).toBe(2) + expect( + selectorDefaultParametricArgsWeakMap.dependencyRecomputations() + ).toBe(2) for (let i = 0; i < 10; i++) { selectorDefaultParametricArgsWeakMap(store.getState(), 1) selectorDefaultParametricArgsWeakMap(store.getState(), 2) @@ -703,6 +738,9 @@ describe('argsMemoize and memoize', () => { selectorDefaultParametricArgsWeakMap(store.getState(), 5) } expect(selectorDefaultParametricArgsWeakMap.recomputations()).toBe(5) + expect( + selectorDefaultParametricArgsWeakMap.dependencyRecomputations() + ).toBe(5) for (let i = 0; i < 10; i++) { selectorDefaultParametric(store.getState(), 1) selectorDefaultParametric(store.getState(), 2) @@ -711,6 +749,7 @@ describe('argsMemoize and memoize', () => { selectorDefaultParametric(store.getState(), 5) } expect(selectorDefaultParametric.recomputations()).toBe(54) + expect(selectorDefaultParametric.dependencyRecomputations()).toBe(54) for (let i = 0; i < 10; i++) { selectorDefaultParametricWeakMap(store.getState(), 1) selectorDefaultParametricWeakMap(store.getState(), 2) @@ -719,6 +758,7 @@ describe('argsMemoize and memoize', () => { selectorDefaultParametricWeakMap(store.getState(), 5) } expect(selectorDefaultParametricWeakMap.recomputations()).toBe(5) + expect(selectorDefaultParametricWeakMap.dependencyRecomputations()).toBe(50) }) localTest('passing argsMemoize to createSelectorCreator', ({ store }) => { @@ -765,7 +805,9 @@ describe('argsMemoize and memoize', () => { ]) ).to.be.an('array').that.is.not.empty expect(selectorMicroMemoize.recomputations()).to.be.a('number') + expect(selectorMicroMemoize.dependencyRecomputations()).to.be.a('number') expect(selectorMicroMemoize.resetRecomputations()).toBe(0) + expect(selectorMicroMemoize.resetDependencyRecomputations()).toBe(0) expect(selectorMicroMemoize.resultFunc).to.be.a('function') expect( selectorMicroMemoize.resultFunc([ @@ -829,7 +871,13 @@ describe('argsMemoize and memoize', () => { ]) ).to.be.an('array').that.is.not.empty expect(selectorMicroMemoizeOverridden.recomputations()).to.be.a('number') + expect(selectorMicroMemoizeOverridden.dependencyRecomputations()).to.be.a( + 'number' + ) expect(selectorMicroMemoizeOverridden.resetRecomputations()).toBe(0) + expect(selectorMicroMemoizeOverridden.resetDependencyRecomputations()).toBe( + 0 + ) expect( selectorMicroMemoizeOverridden.resultFunc([ { @@ -904,9 +952,15 @@ describe('argsMemoize and memoize', () => { expect( selectorMicroMemoizeOverrideArgsMemoizeOnly.recomputations() ).to.be.a('number') + expect( + selectorMicroMemoizeOverrideArgsMemoizeOnly.dependencyRecomputations() + ).to.be.a('number') expect( selectorMicroMemoizeOverrideArgsMemoizeOnly.resetRecomputations() ).toBe(0) + expect( + selectorMicroMemoizeOverrideArgsMemoizeOnly.resetDependencyRecomputations() + ).toBe(0) expect( selectorMicroMemoizeOverrideArgsMemoizeOnly.resultFunc([ { @@ -976,9 +1030,15 @@ describe('argsMemoize and memoize', () => { expect(selectorMicroMemoizeOverrideMemoizeOnly.recomputations()).to.be.a( 'number' ) + expect( + selectorMicroMemoizeOverrideMemoizeOnly.dependencyRecomputations() + ).to.be.a('number') expect(selectorMicroMemoizeOverrideMemoizeOnly.resetRecomputations()).toBe( 0 ) + expect( + selectorMicroMemoizeOverrideMemoizeOnly.resetDependencyRecomputations() + ).toBe(0) expect( selectorMicroMemoizeOverrideMemoizeOnly.resultFunc([ { @@ -1009,9 +1069,7 @@ describe('argsMemoize and memoize', () => { const selector1 = createSelector( [(state: RootState) => state.todos], todos => todos.map(({ id }) => id), - { - memoize: weakMapMemoize - } + { memoize: weakMapMemoize } ) expect(() => //@ts-expect-error @@ -1058,54 +1116,27 @@ describe('argsMemoize and memoize', () => { (state: RootState) => state.users, (state: RootState) => state.users, (state: RootState, id: number) => state.users, - // (state: RootState) => ({ ...state.users }), users => { - console.log('run') return users.user.details.preferences.notifications.push.frequency }, { inputStabilityCheck: 'never' } ) - let start = performance.now() + const start = performance.now() for (let i = 0; i < 10_000_000; i++) { selectorDefault(state) } - console.log(performance.now() - start) + expect(performance.now() - start).toBeLessThan(1000) selectorDefault1(state) - const element2 = store.getState() + const stateBeforeChange = store.getState() selectorDefault2(store.getState(), 0) - const element = store.getState() + const stateBeforeChange1 = store.getState() store.dispatch(toggleCompleted(0)) - const element1 = store.getState() - console.log(element === element1) - console.log(element.alerts === element1.alerts) - console.log(element.todos[1] === element1.todos[1]) - console.log(element === element2) - console.log(element.alerts === element2.alerts) - selectorDefault2(store.getState(), 0) - selectorDefault2(store.getState(), 0) - selectorDefault2(store.getState(), 0) - start = performance.now() - for (let i = 0; i < 100_000_000; i++) { - selector(state) - } - console.log(selector.memoize.name, performance.now() - start) - start = performance.now() - for (let i = 0; i < 100_000_000; i++) { - selector1(state) - } - console.log(selector1.memoize.name, performance.now() - start) - start = performance.now() - for (let i = 0; i < 100; i++) { - selectorDefault2(store.getState(), 0) - // selectorDefault2({ ...state }, 0) - // selectorDefault2({ users: { user: { id: 0, status: '', details: { preferences: { notifications: { push: { frequency: '' } } } } } } }) - } - console.log( - selectorDefault2.memoize.name, - performance.now() - start, - selectorDefault2.recomputations(), - called - ) + const stateAfterChange = store.getState() + expect(stateBeforeChange1).not.toBe(stateAfterChange) + expect(stateBeforeChange1.alerts).toBe(stateAfterChange.alerts) + expect(stateBeforeChange1.todos[1]).toBe(stateAfterChange.todos[1]) + expect(stateBeforeChange1).toBe(stateBeforeChange) + expect(stateBeforeChange1.alerts).toBe(stateBeforeChange.alerts) } ) }) diff --git a/test/testUtils.ts b/test/testUtils.ts index f89a7507a..faeb41447 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -144,10 +144,6 @@ export const pushToTodos = (howMany: number) => { pushToTodos(200) -// for (let i = todoStateLength; i < 200; i++) { -// todoState.push(createTodoItem(i)) -// } - const alertState = [ { id: 0, @@ -419,19 +415,17 @@ export const localTest = test.extend({ state }) -export const resetSelector = >( - selector: Pick +export const resetSelector = ( + selector: S ) => { selector.clearCache() selector.resetRecomputations() + selector.resetDependencyRecomputations() selector.memoizedResultFunc.clearCache() } export const logRecomputations = < - S extends Selector & { - recomputations: () => number - dependencyRecomputations: () => number - } + S extends OutputSelector >( selector: S ) => { From 3912374dc310dc7ea9ff043f793db0f012e75d65 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sat, 18 Nov 2023 19:17:03 -0600 Subject: [PATCH 39/50] Fix title of tests in `weakmapMemoize.spec.ts`. --- test/weakmapMemoize.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/weakmapMemoize.spec.ts b/test/weakmapMemoize.spec.ts index b8d91023e..999d36248 100644 --- a/test/weakmapMemoize.spec.ts +++ b/test/weakmapMemoize.spec.ts @@ -23,7 +23,7 @@ for (let i = 0; i < numOfStates; i++) { states.push({ a: 1, b: 2 }) } -describe('Basic selector behavior with autotrack', () => { +describe('Basic selector behavior with weakMapMemoize', () => { const createSelector = createSelectorCreator(weakMapMemoize) test('basic selector', () => { From 6f94ea735cf3b50a006ae4c186037c78ac2a060c Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sat, 18 Nov 2023 19:18:02 -0600 Subject: [PATCH 40/50] Add "esModuleInterop" to tsconfig for vitest type tests. --- type-tests/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/type-tests/tsconfig.json b/type-tests/tsconfig.json index 7c516c1b4..85f60db12 100644 --- a/type-tests/tsconfig.json +++ b/type-tests/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "module": "commonjs", + "esModuleInterop": true, "strict": true, "target": "ES2015", "declaration": true, From a031621bd33873e6005824ff421d0e1f32bcfda4 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sat, 18 Nov 2023 19:19:03 -0600 Subject: [PATCH 41/50] Add initial vitest type tests for `createSelectorCreator` and `createStructuredSelector`. --- type-tests/createSelectorCreator.test-d.ts | 58 +++++++++++++++++++ type-tests/createStructuredSelector.test-d.ts | 43 ++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 type-tests/createSelectorCreator.test-d.ts create mode 100644 type-tests/createStructuredSelector.test-d.ts diff --git a/type-tests/createSelectorCreator.test-d.ts b/type-tests/createSelectorCreator.test-d.ts new file mode 100644 index 000000000..da5897277 --- /dev/null +++ b/type-tests/createSelectorCreator.test-d.ts @@ -0,0 +1,58 @@ +import lodashMemoize from 'lodash/memoize' +import memoizeOne from 'memoize-one' +import microMemoize from 'micro-memoize' +import { + createSelectorCreator, + defaultMemoize, + unstable_autotrackMemoize as autotrackMemoize, + weakMapMemoize +} from 'reselect' +import { describe, test } from 'vitest' + +interface RootState { + todos: { id: number; completed: boolean }[] + alerts: { id: number; read: boolean }[] +} + +const state: RootState = { + todos: [ + { id: 0, completed: false }, + { id: 1, completed: true } + ], + alerts: [ + { id: 0, read: false }, + { id: 1, read: true } + ] +} + +describe('createSelectorCreator', () => { + test('options object as argument', () => { + const createSelectorDefault = createSelectorCreator({ + memoize: defaultMemoize + }) + const createSelectorWeakMap = createSelectorCreator({ + memoize: weakMapMemoize + }) + const createSelectorAutotrack = createSelectorCreator({ + memoize: autotrackMemoize + }) + const createSelectorMicro = createSelectorCreator({ + memoize: microMemoize + }) + const createSelectorOne = createSelectorCreator({ + memoize: memoizeOne + }) + const createSelectorLodash = createSelectorCreator({ + memoize: lodashMemoize + }) + }) + + test('memoize function as argument', () => { + const createSelectorDefault = createSelectorCreator(defaultMemoize) + const createSelectorWeakMap = createSelectorCreator(weakMapMemoize) + const createSelectorAutotrack = createSelectorCreator(autotrackMemoize) + const createSelectorMicro = createSelectorCreator(microMemoize) + const createSelectorOne = createSelectorCreator(memoizeOne) + const createSelectorLodash = createSelectorCreator(lodashMemoize) + }) +}) diff --git a/type-tests/createStructuredSelector.test-d.ts b/type-tests/createStructuredSelector.test-d.ts new file mode 100644 index 000000000..13f16fbfe --- /dev/null +++ b/type-tests/createStructuredSelector.test-d.ts @@ -0,0 +1,43 @@ +import type { TypedStructuredSelectorCreator } from 'reselect' +import { createStructuredSelector } from 'reselect' +import { describe, test } from 'vitest' + +interface RootState { + todos: { id: number; completed: boolean }[] + alerts: { id: number; read: boolean }[] +} + +const state: RootState = { + todos: [ + { id: 0, completed: false }, + { id: 1, completed: true } + ], + alerts: [ + { id: 0, read: false }, + { id: 1, read: true } + ] +} + +describe('createStructuredSelector', () => { + test('TypedStructuredSelectorCreator', () => { + const typedStructuredSelectorCreator: TypedStructuredSelectorCreator = + createStructuredSelector + const structuredSelector = typedStructuredSelectorCreator({ + todos: state => state.todos, + alerts: state => state.alerts + }) + structuredSelector(state).alerts + structuredSelector(state).todos + }) + test('parametric', () => { + const structuredSelector = createStructuredSelector({ + todos: (state: RootState) => state.todos, + alerts: (state: RootState) => state.alerts, + todoById: (state: RootState, id: number) => state.todos[id] + }) + structuredSelector(state, 0).alerts + structuredSelector(state, 0).todoById.id + structuredSelector(state, 0).todos + const { alerts, todos, todoById } = structuredSelector(state, 0) + }) +}) From 72153049f430074cb53e979d39fc9092415a27be Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sat, 18 Nov 2023 19:20:53 -0600 Subject: [PATCH 42/50] Fix benchmarks for `orderOfExecution.bench.ts` and `weakMapMemoize.bench.ts`. --- test/benchmarks/orderOfExecution.bench.ts | 31 +- test/benchmarks/weakMapMemoize.bench.ts | 348 ++++++++-------------- 2 files changed, 138 insertions(+), 241 deletions(-) diff --git a/test/benchmarks/orderOfExecution.bench.ts b/test/benchmarks/orderOfExecution.bench.ts index 7e0b76642..a4f307e8b 100644 --- a/test/benchmarks/orderOfExecution.bench.ts +++ b/test/benchmarks/orderOfExecution.bench.ts @@ -43,7 +43,10 @@ describe('less in input selectors vs more in input selectors', () => { time: 0 } setFunctionNames({ selectorGood, selectorBad, nonMemoized }) - const createOptions = (selector: S) => { + const createOptions = ( + selector: S, + commonOptions: Options = {} + ) => { const options: Options = { setup: (task, mode) => { if (mode === 'warmup') return @@ -57,27 +60,21 @@ describe('less in input selectors vs more in input selectors', () => { } } } - return options + return { ...commonOptions, ...options } } bench( selectorGood, () => { selectorGood(store.getState(), 0) }, - { - ...commonOptions, - ...createOptions(selectorGood) - } + createOptions(selectorGood, commonOptions) ) bench( selectorBad, () => { selectorBad(store.getState(), 0) }, - { - ...commonOptions, - ...createOptions(selectorBad) - } + createOptions(selectorBad, commonOptions) ) bench( nonMemoized, @@ -120,13 +117,9 @@ describe('using standalone memoization methods vs createSelector', () => { return state.users.appSettings }) setFunctionNames({ fieldAccessor, fieldAccessor1 }) - const createOptions = < - S extends Selector & { - recomputations: () => number - dependencyRecomputations: () => number - } - >( - selector: S + const createOptions = ( + selector: S, + commonOptions: Options = {} ) => { const options: Options = { setup: (task, mode) => { @@ -141,14 +134,14 @@ describe('using standalone memoization methods vs createSelector', () => { } } } - return options + return { ...commonOptions, ...options } } bench( fieldAccessor, () => { fieldAccessor(store.getState()) }, - { ...commonOptions, ...createOptions(fieldAccessor) } + createOptions(fieldAccessor, commonOptions) ) bench( fieldAccessor1, diff --git a/test/benchmarks/weakMapMemoize.bench.ts b/test/benchmarks/weakMapMemoize.bench.ts index 7566613d0..8d705c477 100644 --- a/test/benchmarks/weakMapMemoize.bench.ts +++ b/test/benchmarks/weakMapMemoize.bench.ts @@ -1,4 +1,4 @@ -import type { Selector } from 'reselect' +import type { OutputSelector, Selector } from 'reselect' import { createSelector, unstable_autotrackMemoize as autotrackMemoize, @@ -6,20 +6,23 @@ import { } from 'reselect' import { bench } from 'vitest' import type { RootState } from '../testUtils' -import { setFunctionNames, setupStore } from '../testUtils' +import { + logRecomputations, + resetSelector, + setFunctionNames, + setupStore +} from '../testUtils' import type { Options } from 'tinybench' -const store = setupStore() -const state = store.getState() -const arr = Array.from({ length: 30 }, (e, i) => i) - -const commonOptions: Options = { - iterations: 10, - time: 0 -} - -describe('weakMapMemoize vs defaultMemoize', () => { +describe('weakMapMemoize vs others', () => { + const store = setupStore() + const state = store.getState() + const arr = Array.from({ length: 30 }, (e, i) => i) + const commonOptions: Options = { + iterations: 10, + time: 0 + } const selectorDefault = createSelector( [(state: RootState) => state.todos, (state: RootState, id: number) => id], (todos, id) => todos.map(todo => todo.id === id) @@ -90,7 +93,7 @@ describe('weakMapMemoize vs defaultMemoize', () => { selectorBothAutotrack, nonMemoizedSelector }) - const runSelector = (selector: Selector) => { + const runSelector = (selector: S) => { arr.forEach((e, i) => { selector(state, e) }) @@ -98,237 +101,102 @@ describe('weakMapMemoize vs defaultMemoize', () => { selector(state, e) }) } - bench( - selectorDefault, - () => { - runSelector(selectorDefault) - }, - { - ...commonOptions, + + const createOptions = ( + selector: S, + commonOptions: Options = {} + ) => { + const options: Options = { setup: (task, mode) => { if (mode === 'warmup') return - selectorDefault.clearCache() - selectorDefault.resetRecomputations() - selectorDefault.memoizedResultFunc.clearCache() + resetSelector(selector) task.opts = { afterAll: () => { - console.log( - `${selectorDefault.name} recomputations after:`, - selectorDefault.recomputations() - 1 - ) + logRecomputations(selector) } } } } + return { ...commonOptions, ...options } + } + bench( + selectorDefault, + () => { + runSelector(selectorDefault) + }, + createOptions(selectorDefault, commonOptions) ) bench( selectorDefaultWithCacheSize, () => { runSelector(selectorDefaultWithCacheSize) }, - { - ...commonOptions, - setup: (task, mode) => { - if (mode === 'warmup') return - selectorDefaultWithCacheSize.clearCache() - selectorDefaultWithCacheSize.resetRecomputations() - selectorDefaultWithCacheSize.memoizedResultFunc.clearCache() - task.opts = { - afterAll: () => { - console.log( - `${selectorDefaultWithCacheSize.name} recomputations after:`, - selectorDefaultWithCacheSize.recomputations() - 1 - ) - } - } - } - } + createOptions(selectorDefaultWithCacheSize, commonOptions) + ) bench( selectorDefaultWithArgsCacheSize, () => { runSelector(selectorDefaultWithArgsCacheSize) }, - { - ...commonOptions, - setup: (task, mode) => { - if (mode === 'warmup') return - selectorDefaultWithArgsCacheSize.clearCache() - selectorDefaultWithArgsCacheSize.resetRecomputations() - selectorDefaultWithArgsCacheSize.memoizedResultFunc.clearCache() - task.opts = { - afterAll: () => { - console.log( - `${selectorDefaultWithArgsCacheSize.name} recomputations after:`, - selectorDefaultWithArgsCacheSize.recomputations() - 1 - ) - } - } - } - } + createOptions(selectorDefaultWithArgsCacheSize, commonOptions) + ) bench( selectorDefaultWithBothCacheSize, () => { runSelector(selectorDefaultWithBothCacheSize) }, - { - ...commonOptions, - setup: (task, mode) => { - if (mode === 'warmup') return - selectorDefaultWithBothCacheSize.clearCache() - selectorDefaultWithBothCacheSize.resetRecomputations() - selectorDefaultWithBothCacheSize.memoizedResultFunc.clearCache() - task.opts = { - afterAll: () => { - console.log( - `${selectorDefaultWithBothCacheSize.name} recomputations after:`, - selectorDefaultWithBothCacheSize.recomputations() - 1 - ) - } - } - } - } + createOptions(selectorDefaultWithBothCacheSize, commonOptions) + ) bench( selectorWeakMap, () => { runSelector(selectorWeakMap) }, - { - ...commonOptions, - setup: (task, mode) => { - if (mode === 'warmup') return - selectorWeakMap.clearCache() - selectorWeakMap.resetRecomputations() - selectorWeakMap.memoizedResultFunc.clearCache() - task.opts = { - afterAll: () => { - console.log( - `${selectorWeakMap.name} recomputations after:`, - selectorWeakMap.recomputations() - 1, - selectorWeakMap.dependencyRecomputations() - ) - } - } - } - } + createOptions(selectorWeakMap, commonOptions) + ) bench( selectorArgsWeakMap, () => { runSelector(selectorArgsWeakMap) }, - { - ...commonOptions, - setup: (task, mode) => { - if (mode === 'warmup') return - selectorArgsWeakMap.clearCache() - selectorArgsWeakMap.resetRecomputations() - selectorArgsWeakMap.memoizedResultFunc.clearCache() - task.opts = { - afterAll: () => { - console.log( - `${selectorArgsWeakMap.name} recomputations after:`, - selectorArgsWeakMap.recomputations() - 1, - selectorArgsWeakMap.dependencyRecomputations() - ) - } - } - } - } + createOptions(selectorArgsWeakMap, commonOptions) + ) bench( selectorBothWeakMap, () => { runSelector(selectorBothWeakMap) }, - { - ...commonOptions, - setup: (task, mode) => { - if (mode === 'warmup') return - selectorBothWeakMap.clearCache() - selectorBothWeakMap.resetRecomputations() - selectorBothWeakMap.memoizedResultFunc.clearCache() - task.opts = { - afterAll: () => { - console.log( - `${selectorBothWeakMap.name} recomputations after:`, - selectorBothWeakMap.recomputations() - 1 - ) - } - } - } - } + createOptions(selectorBothWeakMap, commonOptions) + ) bench( selectorAutotrack, () => { runSelector(selectorAutotrack) }, - { - ...commonOptions, - setup: (task, mode) => { - if (mode === 'warmup') return - selectorAutotrack.clearCache() - selectorAutotrack.resetRecomputations() - selectorAutotrack.memoizedResultFunc.clearCache() - task.opts = { - afterAll: () => { - console.log( - `${selectorAutotrack.name} recomputations after:`, - selectorAutotrack.recomputations() - 1 - ) - } - } - } - } + createOptions(selectorAutotrack, commonOptions) + ) bench( selectorArgsAutotrack, () => { runSelector(selectorArgsAutotrack) }, - { - ...commonOptions, - setup: (task, mode) => { - if (mode === 'warmup') return - selectorArgsAutotrack.clearCache() - selectorArgsAutotrack.resetRecomputations() - selectorArgsAutotrack.memoizedResultFunc.clearCache() - task.opts = { - afterAll: () => { - console.log( - `${selectorArgsAutotrack.name} recomputations after:`, - selectorArgsAutotrack.recomputations() - 1 - ) - } - } - } - } + createOptions(selectorArgsAutotrack, commonOptions) + ) bench( selectorBothAutotrack, () => { runSelector(selectorBothAutotrack) }, - { - ...commonOptions, - setup: (task, mode) => { - if (mode === 'warmup') return - selectorBothAutotrack.clearCache() - selectorBothAutotrack.resetRecomputations() - selectorBothAutotrack.memoizedResultFunc.clearCache() - task.opts = { - afterAll: () => { - console.log( - `${selectorBothAutotrack.name} recomputations after:`, - selectorBothAutotrack.recomputations() - 1 - ) - } - } - } - } + createOptions(selectorBothAutotrack, commonOptions) + ) bench( nonMemoizedSelector, @@ -340,6 +208,15 @@ describe('weakMapMemoize vs defaultMemoize', () => { }) describe('weakMapMemoize simple examples', () => { + const store = setupStore() + const state = store.getState() + const arr = Array.from({ length: 30 }, (e, i) => i) + const commonOptions: Options = { + warmupIterations: 0, + warmupTime: 0, + iterations: 1, + time: 0 + } const selectorDefault = createSelector( [(state: RootState) => state.todos], todos => todos.map(({ id }) => id) @@ -361,73 +238,100 @@ describe('weakMapMemoize simple examples', () => { selectorAutotrack }) - bench( - selectorDefault, - () => { - selectorDefault(store.getState()) - }, - { - ...commonOptions, + const createOptions = (selector: S) => { + const options: Options = { setup: (task, mode) => { if (mode === 'warmup') return - selectorDefault.clearCache() - selectorDefault.resetRecomputations() - selectorDefault.memoizedResultFunc.clearCache() + resetSelector(selector) task.opts = { afterAll: () => { - console.log( - `${selectorDefault.name} recomputations after:`, - selectorDefault.recomputations() - 1 - ) + logRecomputations(selector) } } } } + return { ...commonOptions, ...options } + } + + bench( + selectorDefault, + () => { + selectorDefault(store.getState()) + }, + createOptions(selectorDefault) ) bench( selectorWeakMap, () => { selectorWeakMap(store.getState()) }, - { - ...commonOptions, - setup: (task, mode) => { - if (mode === 'warmup') return - selectorWeakMap.clearCache() - selectorWeakMap.resetRecomputations() - selectorWeakMap.memoizedResultFunc.clearCache() - task.opts = { - afterAll: () => { - console.log( - `${selectorWeakMap.name} recomputations after:`, - selectorWeakMap.recomputations() - 1 - ) - } - } - } - } + createOptions(selectorWeakMap) ) bench( selectorAutotrack, () => { selectorAutotrack(store.getState()) }, - { - ...commonOptions, + createOptions(selectorAutotrack) + ) +}) + +describe.skip('weakMapMemoize vs defaultMemoize memory leak', () => { + const store = setupStore() + const state = store.getState() + const arr = Array.from({ length: 2_000_000 }, (e, i) => i) + const commonOptions: Options = { + warmupIterations: 0, + warmupTime: 0, + iterations: 1, + time: 0 + } + const selectorDefault = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + todos => todos.map(({ id }) => id) + ) + const selectorWeakMap = createSelector( + [(state: RootState) => state.todos, (state: RootState, id: number) => id], + todos => todos.map(({ id }) => id), + { argsMemoize: weakMapMemoize, memoize: weakMapMemoize } + ) + const runSelector = (selector: S) => { + arr.forEach((e, i) => { + selector(state, e) + }) + arr.forEach((e, i) => { + selector(state, e) + }) + } + setFunctionNames({ selectorDefault, selectorWeakMap }) + const createOptions = ( + selector: S, + commonOptions: Options = {} + ) => { + const options: Options = { setup: (task, mode) => { if (mode === 'warmup') return - selectorAutotrack.clearCache() - selectorAutotrack.resetRecomputations() - selectorAutotrack.memoizedResultFunc.clearCache() task.opts = { afterAll: () => { - console.log( - `${selectorAutotrack.name} recomputations after:`, - selectorAutotrack.recomputations() - 1 - ) + logRecomputations(selector) } } } } + return { ...commonOptions, ...options } + } + bench( + selectorDefault, + () => { + runSelector(selectorDefault) + }, + createOptions(selectorDefault, commonOptions) + ) + bench( + selectorWeakMap, + () => { + runSelector(selectorWeakMap) + }, + createOptions(selectorWeakMap, commonOptions) ) }) From 4595ef6c91ab0ee39e7fddb1807c5b377e3051e2 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sat, 18 Nov 2023 19:22:15 -0600 Subject: [PATCH 43/50] Update `yarn.lock`. --- yarn.lock | 150 +++++++++++++++++++++++++++--------------------------- 1 file changed, 75 insertions(+), 75 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3a1dc4d72..10df972ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -364,10 +364,10 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:8.53.0": - version: 8.53.0 - resolution: "@eslint/js@npm:8.53.0" - checksum: e0d5cfb0000aaee237c8e6d6d6e366faa60b1ef7f928ce17778373aa44d3b886368f6d5e1f97f913f0f16801aad016db8b8df78418c9d18825c15590328028af +"@eslint/js@npm:8.54.0": + version: 8.54.0 + resolution: "@eslint/js@npm:8.54.0" + checksum: 6d88a6f711ef0133566b5340e3178a178fbb297585766460f195d0a9db85688f1e5cf8559fd5748aeb3131e2096c66595b323d8edab22df015acda68f1ebde92 languageName: node linkType: hard @@ -537,86 +537,86 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.4.0": - version: 4.4.0 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.4.0" +"@rollup/rollup-android-arm-eabi@npm:4.5.0": + version: 4.5.0 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.5.0" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.4.0": - version: 4.4.0 - resolution: "@rollup/rollup-android-arm64@npm:4.4.0" +"@rollup/rollup-android-arm64@npm:4.5.0": + version: 4.5.0 + resolution: "@rollup/rollup-android-arm64@npm:4.5.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.4.0": - version: 4.4.0 - resolution: "@rollup/rollup-darwin-arm64@npm:4.4.0" +"@rollup/rollup-darwin-arm64@npm:4.5.0": + version: 4.5.0 + resolution: "@rollup/rollup-darwin-arm64@npm:4.5.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.4.0": - version: 4.4.0 - resolution: "@rollup/rollup-darwin-x64@npm:4.4.0" +"@rollup/rollup-darwin-x64@npm:4.5.0": + version: 4.5.0 + resolution: "@rollup/rollup-darwin-x64@npm:4.5.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.4.0": - version: 4.4.0 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.4.0" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.5.0": + version: 4.5.0 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.5.0" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.4.0": - version: 4.4.0 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.4.0" +"@rollup/rollup-linux-arm64-gnu@npm:4.5.0": + version: 4.5.0 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.5.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.4.0": - version: 4.4.0 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.4.0" +"@rollup/rollup-linux-arm64-musl@npm:4.5.0": + version: 4.5.0 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.5.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.4.0": - version: 4.4.0 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.4.0" +"@rollup/rollup-linux-x64-gnu@npm:4.5.0": + version: 4.5.0 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.5.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.4.0": - version: 4.4.0 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.4.0" +"@rollup/rollup-linux-x64-musl@npm:4.5.0": + version: 4.5.0 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.5.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.4.0": - version: 4.4.0 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.4.0" +"@rollup/rollup-win32-arm64-msvc@npm:4.5.0": + version: 4.5.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.5.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.4.0": - version: 4.4.0 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.4.0" +"@rollup/rollup-win32-ia32-msvc@npm:4.5.0": + version: 4.5.0 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.5.0" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.4.0": - version: 4.4.0 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.4.0" +"@rollup/rollup-win32-x64-msvc@npm:4.5.0": + version: 4.5.0 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.5.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -686,11 +686,11 @@ __metadata: linkType: hard "@types/node@npm:*": - version: 20.9.0 - resolution: "@types/node@npm:20.9.0" + version: 20.9.2 + resolution: "@types/node@npm:20.9.2" dependencies: undici-types: ~5.26.4 - checksum: bfd927da6bff8a32051fa44bb47ca32a56d2c8bc8ba0170770f181cc1fa3c0b05863c9b930f0ba8604a48d5eb0d319166601709ca53bf2deae0025d8b6c6b8a3 + checksum: 5bbb8fb2248fc5c5c4071d9809fb9af85997677c07124d65665202b53283a3b7bdff26fb844e9ee407e3847dfce6399c2b01e3329ea44a4b720647b1b987c678 languageName: node linkType: hard @@ -1860,13 +1860,13 @@ __metadata: linkType: hard "eslint@npm:^8.0.1": - version: 8.53.0 - resolution: "eslint@npm:8.53.0" + version: 8.54.0 + resolution: "eslint@npm:8.54.0" dependencies: "@eslint-community/eslint-utils": ^4.2.0 "@eslint-community/regexpp": ^4.6.1 "@eslint/eslintrc": ^2.1.3 - "@eslint/js": 8.53.0 + "@eslint/js": 8.54.0 "@humanwhocodes/config-array": ^0.11.13 "@humanwhocodes/module-importer": ^1.0.1 "@nodelib/fs.walk": ^1.2.8 @@ -1903,7 +1903,7 @@ __metadata: text-table: ^0.2.0 bin: eslint: bin/eslint.js - checksum: 2da808655c7aa4b33f8970ba30d96b453c3071cc4d6cd60d367163430677e32ff186b65270816b662d29139283138bff81f28dddeb2e73265495245a316ed02c + checksum: 7e876e9da2a18a017271cf3733d05a3dfbbe469272d75753408c6ea5b1646c71c6bb18cb91e10ca930144c32c1ce3701e222f1ae6784a3975a69f8f8aa68e49f languageName: node linkType: hard @@ -2440,9 +2440,9 @@ __metadata: linkType: hard "ignore@npm:^5.1.8, ignore@npm:^5.2.0": - version: 5.2.4 - resolution: "ignore@npm:5.2.4" - checksum: 3d4c309c6006e2621659311783eaea7ebcd41fe4ca1d78c91c473157ad6666a57a2df790fe0d07a12300d9aac2888204d7be8d59f9aaf665b1c7fcdb432517ef + version: 5.3.0 + resolution: "ignore@npm:5.3.0" + checksum: 2736da6621f14ced652785cb05d86301a66d70248597537176612bd0c8630893564bd5f6421f8806b09e8472e75c591ef01672ab8059c07c6eb2c09cefe04bf9 languageName: node linkType: hard @@ -3951,21 +3951,21 @@ __metadata: linkType: hard "rollup@npm:^4.2.0": - version: 4.4.0 - resolution: "rollup@npm:4.4.0" - dependencies: - "@rollup/rollup-android-arm-eabi": 4.4.0 - "@rollup/rollup-android-arm64": 4.4.0 - "@rollup/rollup-darwin-arm64": 4.4.0 - "@rollup/rollup-darwin-x64": 4.4.0 - "@rollup/rollup-linux-arm-gnueabihf": 4.4.0 - "@rollup/rollup-linux-arm64-gnu": 4.4.0 - "@rollup/rollup-linux-arm64-musl": 4.4.0 - "@rollup/rollup-linux-x64-gnu": 4.4.0 - "@rollup/rollup-linux-x64-musl": 4.4.0 - "@rollup/rollup-win32-arm64-msvc": 4.4.0 - "@rollup/rollup-win32-ia32-msvc": 4.4.0 - "@rollup/rollup-win32-x64-msvc": 4.4.0 + version: 4.5.0 + resolution: "rollup@npm:4.5.0" + dependencies: + "@rollup/rollup-android-arm-eabi": 4.5.0 + "@rollup/rollup-android-arm64": 4.5.0 + "@rollup/rollup-darwin-arm64": 4.5.0 + "@rollup/rollup-darwin-x64": 4.5.0 + "@rollup/rollup-linux-arm-gnueabihf": 4.5.0 + "@rollup/rollup-linux-arm64-gnu": 4.5.0 + "@rollup/rollup-linux-arm64-musl": 4.5.0 + "@rollup/rollup-linux-x64-gnu": 4.5.0 + "@rollup/rollup-linux-x64-musl": 4.5.0 + "@rollup/rollup-win32-arm64-msvc": 4.5.0 + "@rollup/rollup-win32-ia32-msvc": 4.5.0 + "@rollup/rollup-win32-x64-msvc": 4.5.0 fsevents: ~2.3.2 dependenciesMeta: "@rollup/rollup-android-arm-eabi": @@ -3996,7 +3996,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 90fdc55bfd247dc07c04901e1b802083516c0982e6b32d2fbccee6968fb944482207accdea2f5dc01198d36a5eb9356f9be451942c45c006a87a3bf50add1803 + checksum: 942f08783bf45e623d177c3b58701e463952b9388822d4f8aadf4e3b465141837ad7f8c85737f7709c2dd0063782c9ce528f71dd33ca6dc3a2d112991d6cc097 languageName: node linkType: hard @@ -4236,9 +4236,9 @@ __metadata: linkType: hard "std-env@npm:^3.3.3": - version: 3.4.3 - resolution: "std-env@npm:3.4.3" - checksum: bef186fb2baddda31911234b1e58fa18f181eb6930616aaec3b54f6d5db65f2da5daaa5f3b326b98445a7d50ca81d6fe8809ab4ebab85ecbe4a802f1b40921bf + version: 3.5.0 + resolution: "std-env@npm:3.5.0" + checksum: 8eba87eab2d6933e0575f13a65a359952a2e3e8c4d24eb55beac5500fe0403b3482c7b59a5de8d035ae13d390c76dd6c677772f9d2a89ea7cf39ae267b71bdd3 languageName: node linkType: hard @@ -4653,9 +4653,9 @@ __metadata: linkType: hard "ufo@npm:^1.3.0": - version: 1.3.1 - resolution: "ufo@npm:1.3.1" - checksum: 2db2f9d24e3f572ddb9b2f4415eda679fd366cbb9eec4c56996651323737f17528b4aab2bb45c5f2effff2304f9b0c46e0981aee3e48f38ac51106a8993dff31 + version: 1.3.2 + resolution: "ufo@npm:1.3.2" + checksum: f1180bb715ff4dd46152fd4dec41c731e84d7b9eaf1432548a0210b2f7e0cd29de125ac88e582c6a079d8ae5bc9ab04ef2bdbafe125086480b10c1006b81bfce languageName: node linkType: hard @@ -4738,8 +4738,8 @@ __metadata: linkType: hard "vite@npm:^3.0.0 || ^4.0.0 || ^5.0.0-0, vite@npm:^3.1.0 || ^4.0.0 || ^5.0.0-0": - version: 5.0.0-beta.18 - resolution: "vite@npm:5.0.0-beta.18" + version: 5.0.0 + resolution: "vite@npm:5.0.0" dependencies: esbuild: ^0.19.3 fsevents: ~2.3.3 @@ -4773,7 +4773,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 9481814791af3932266fa05112779d51d2f330b964d3472ca3373861b0a3c33169c55b5bc5fa82752f7d046ff320dfaaa0db77f2b78ec8cbe3dd6310eb7e8d69 + checksum: 1f953b062593b072f0e718384e1ff3307b548235ff8c4016fcaa85c09568eb0ba8cd8cfd80e99d940d3bea296b4661b1d0384fe5cb9a996d3e935feb69259755 languageName: node linkType: hard From 3de511034c8caffca08600933245d46a106d065f Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sat, 18 Nov 2023 22:50:22 -0600 Subject: [PATCH 44/50] Remove changes unrelated to docs. --- .gitignore | 4 +- package.json | 5 +- src/autotrackMemoize/autotrackMemoize.ts | 14 +- src/createSelectorCreator.ts | 97 +- src/createStructuredSelector.ts | 153 +-- src/defaultMemoize.ts | 12 +- src/index.ts | 9 +- src/types.ts | 301 +++--- src/utils.ts | 2 +- src/versionedTypes/ts47-mergeParameters.ts | 101 +- src/weakMapMemoize.ts | 75 +- test/autotrackMemoize.spec.ts | 7 +- test/benchmarks/orderOfExecution.bench.ts | 166 ---- test/benchmarks/reselect.bench.ts | 308 ------ test/benchmarks/weakMapMemoize.bench.ts | 337 ------- test/createStructuredSelector.spec.ts | 41 - test/reselect.bench.ts | 0 test/reselect.spec.ts | 246 ++--- test/selectorUtils.spec.ts | 1 - test/testUtils.ts | 252 +---- test/tsconfig.json | 12 +- test/weakmapMemoize.spec.ts | 4 +- tsconfig.json | 3 +- type-tests/argsMemoize.test-d.ts | 894 ------------------ type-tests/createSelectorCreator.test-d.ts | 58 -- type-tests/createStructuredSelector.test-d.ts | 43 - type-tests/deepNesting.test-d.ts | 320 ------- type-tests/tsconfig.json | 15 - vitest.config.ts | 1 - 29 files changed, 389 insertions(+), 3092 deletions(-) delete mode 100644 test/benchmarks/orderOfExecution.bench.ts delete mode 100644 test/benchmarks/reselect.bench.ts delete mode 100644 test/benchmarks/weakMapMemoize.bench.ts create mode 100644 test/reselect.bench.ts delete mode 100644 type-tests/argsMemoize.test-d.ts delete mode 100644 type-tests/createSelectorCreator.test-d.ts delete mode 100644 type-tests/createStructuredSelector.test-d.ts delete mode 100644 type-tests/deepNesting.test-d.ts delete mode 100644 type-tests/tsconfig.json diff --git a/.gitignore b/.gitignore index d288a6f6e..9876dbe03 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ typescript_test/common.js flow_test/should_fail/flow-typed/index.js.flow flow_test/should_pass/flow-typed/index.js.flow reselect-builds/ -trace + typesversions .cache @@ -24,4 +24,4 @@ typesversions !.yarn/sdks !.yarn/versions .pnp.* -*.tgz +*.tgz \ No newline at end of file diff --git a/package.json b/package.json index d53c4a820..8ca716863 100644 --- a/package.json +++ b/package.json @@ -27,11 +27,9 @@ "format": "prettier --write \"{src,test}/**/*.{js,ts}\" \"docs/**/*.md\"", "lint": "eslint src test", "prepack": "yarn build", - "bench": "vitest --run bench --mode production", + "bench": "vitest --run bench", "test": "node --expose-gc ./node_modules/vitest/dist/cli-wrapper.js run", "test:cov": "vitest run --coverage", - "type-check": "vitest --run typecheck", - "type-check:trace": "vitest --run typecheck && tsc --noEmit -p typescript_test/tsconfig.json --generateTrace trace && npx @typescript/analyze-trace trace && rimraf trace", "test:typescript": "tsc --noEmit -p typescript_test/tsconfig.json" }, "keywords": [ @@ -56,7 +54,6 @@ "@typescript-eslint/eslint-plugin": "5.1.0", "@typescript-eslint/eslint-plugin-tslint": "5.1.0", "@typescript-eslint/parser": "5.1.0", - "@typescript/analyze-trace": "^0.10.1", "eslint": "^8.0.1", "eslint-plugin-react": "^7.26.1", "eslint-plugin-typescript": "0.14.0", diff --git a/src/autotrackMemoize/autotrackMemoize.ts b/src/autotrackMemoize/autotrackMemoize.ts index f2a0a1799..e5d698a0e 100644 --- a/src/autotrackMemoize/autotrackMemoize.ts +++ b/src/autotrackMemoize/autotrackMemoize.ts @@ -5,11 +5,7 @@ import { createCacheKeyComparator, defaultEqualityCheck } from '@internal/defaultMemoize' -import type { - AnyFunction, - DefaultMemoizeFields, - Simplify -} from '@internal/types' +import type { AnyFunction } from '@internal/types' import { createCache } from './autotracking' /** @@ -59,7 +55,7 @@ import { createCache } from './autotracking' * ```ts * import { unstable_autotrackMemoize as autotrackMemoize, createSelectorCreator } from 'reselect' * - * const createSelectorAutotrack = createSelectorCreator({ memoize: autotrackMemoize }) + * const createSelectorAutotrack = createSelectorCreator(autotrackMemoize) * * const selectTodoIds = createSelectorAutotrack( * [(state: RootState) => state.todos], @@ -97,9 +93,7 @@ export function autotrackMemoize(func: Func) { return cache.value } - memoized.clearCache = () => { - return cache.clear() - } + memoized.clearCache = () => cache.clear() - return memoized as Func & Simplify + return memoized as Func & { clearCache: () => void } } diff --git a/src/createSelectorCreator.ts b/src/createSelectorCreator.ts index ff9d46230..85525ca9c 100644 --- a/src/createSelectorCreator.ts +++ b/src/createSelectorCreator.ts @@ -1,3 +1,4 @@ +import type { OutputSelector, Selector, SelectorArray } from 'reselect' import { defaultMemoize } from './defaultMemoize' import type { @@ -8,11 +9,6 @@ import type { GetParamsFromSelectors, GetStateFromSelectors, InterruptRecursion, - OutputSelector, - Selector, - SelectorArray, - SetRequired, - Simplify, StabilityCheckFrequency, UnknownMemoizer } from './types' @@ -86,7 +82,7 @@ export interface CreateSelectorFunction< ...createSelectorArgs: [ ...inputSelectors: InputSelectors, combiner: Combiner, - createSelectorOptions: Simplify< + createSelectorOptions: Partial< CreateSelectorOptions< MemoizeFunction, ArgsMemoizeFunction, @@ -126,7 +122,7 @@ export interface CreateSelectorFunction< >( inputSelectors: [...InputSelectors], combiner: Combiner, - createSelectorOptions?: Simplify< + createSelectorOptions?: Partial< CreateSelectorOptions< MemoizeFunction, ArgsMemoizeFunction, @@ -224,16 +220,11 @@ export function createSelectorCreator< MemoizeFunction extends UnknownMemoizer, ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize >( - options: Simplify< - SetRequired< - CreateSelectorOptions< - typeof defaultMemoize, - typeof defaultMemoize, - MemoizeFunction, - ArgsMemoizeFunction - >, - 'memoize' - > + options: CreateSelectorOptions< + typeof defaultMemoize, + typeof defaultMemoize, + MemoizeFunction, + ArgsMemoizeFunction > ): CreateSelectorFunction @@ -285,29 +276,27 @@ export function createSelectorCreator< ArgsMemoizeFunction extends UnknownMemoizer, MemoizeOrOptions extends | MemoizeFunction - | SetRequired< - CreateSelectorOptions, - 'memoize' - > + | CreateSelectorOptions >( memoizeOrOptions: MemoizeOrOptions, - ...memoizeOptionsFromArgs: MemoizeOrOptions extends SetRequired< - CreateSelectorOptions, - 'memoize' + ...memoizeOptionsFromArgs: MemoizeOrOptions extends CreateSelectorOptions< + MemoizeFunction, + ArgsMemoizeFunction > ? never : DropFirstParameter ) { /** options initially passed into `createSelectorCreator`. */ - const createSelectorCreatorOptions: SetRequired< - CreateSelectorOptions, - 'memoize' - > = typeof memoizeOrOptions === 'function' - ? { - memoize: memoizeOrOptions as MemoizeFunction, - memoizeOptions: memoizeOptionsFromArgs - } - : memoizeOrOptions + const createSelectorCreatorOptions: CreateSelectorOptions< + MemoizeFunction, + ArgsMemoizeFunction + > = + typeof memoizeOrOptions === 'function' + ? { + memoize: memoizeOrOptions as MemoizeFunction, + memoizeOptions: memoizeOptionsFromArgs + } + : memoizeOrOptions const createSelector = < InputSelectors extends SelectorArray, @@ -318,36 +307,41 @@ export function createSelectorCreator< ...createSelectorArgs: [ ...inputSelectors: [...InputSelectors], combiner: Combiner, - createSelectorOptions?: CreateSelectorOptions< - MemoizeFunction, - ArgsMemoizeFunction, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction + createSelectorOptions?: Partial< + CreateSelectorOptions< + MemoizeFunction, + ArgsMemoizeFunction, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > > ] ) => { let recomputations = 0 - let dependencyRecomputations = 0 let lastResult: Result - // Due to the intricacies of rest params, we can't do an optional arg after `...createSelectorArgs`. + // Due to the intricacies of rest params, we can't do an optional arg after `...funcs`. // So, start by declaring the default value here. // (And yes, the words 'memoize' and 'options' appear too many times in this next sequence.) - let directlyPassedOptions: CreateSelectorOptions< - MemoizeFunction, - ArgsMemoizeFunction, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction + let directlyPassedOptions: Partial< + CreateSelectorOptions< + MemoizeFunction, + ArgsMemoizeFunction, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > > = {} // Normally, the result func or "combiner" is the last arg let resultFunc = createSelectorArgs.pop() as | Combiner - | CreateSelectorOptions< - MemoizeFunction, - ArgsMemoizeFunction, - OverrideMemoizeFunction, - OverrideArgsMemoizeFunction + | Partial< + CreateSelectorOptions< + MemoizeFunction, + ArgsMemoizeFunction, + OverrideMemoizeFunction, + OverrideArgsMemoizeFunction + > > // If the result func is actually an _object_, assume it's our options object @@ -401,7 +395,6 @@ export function createSelectorCreator< // If a selector is called with the exact same arguments we don't need to traverse our dependencies again. const selector = argsMemoize(function dependenciesChecker() { - dependencyRecomputations++ /** Return values of input selectors which the `resultFunc` takes as arguments. */ const inputSelectorResults = collectInputSelectorResults( dependencies, @@ -443,8 +436,6 @@ export function createSelectorCreator< resultFunc, memoizedResultFunc, dependencies, - dependencyRecomputations: () => dependencyRecomputations, - resetDependencyRecomputations: () => (dependencyRecomputations = 0), lastResult: () => lastResult, recomputations: () => recomputations, resetRecomputations: () => (recomputations = 0), diff --git a/src/createStructuredSelector.ts b/src/createStructuredSelector.ts index 4a6dc664c..11462b925 100644 --- a/src/createStructuredSelector.ts +++ b/src/createStructuredSelector.ts @@ -3,11 +3,9 @@ import { createSelector } from './createSelectorCreator' import type { CreateSelectorFunction } from './createSelectorCreator' import type { defaultMemoize } from './defaultMemoize' import type { - InterruptRecursion, ObjectValuesToTuple, OutputSelector, Selector, - Simplify, UnknownMemoizer } from './types' import { assertIsObject } from './utils' @@ -67,10 +65,11 @@ interface SelectorsObject { */ export interface StructuredSelectorCreator { /** - * A convenience function that simplifies returning an object - * made up of selector results. + * A convenience function for a common pattern that arises when using Reselect. + * The selector passed to a `connect` decorator often just takes the + * values of its input selectors and maps them to keys in an object. * - * @param inputSelectorsObject - A key value pair consisting of input selectors. + * @param selectorMap - A key value pair consisting of input selectors. * @param selectorCreator - A custom selector creator function. It defaults to `createSelector`. * @returns A memoized structured selector. * @@ -79,22 +78,58 @@ export interface StructuredSelectorCreator { * ```ts * import { createSelector, createStructuredSelector } from 'reselect' * - * interface RootState { + * interface State { * todos: { * id: number - * completed: boolean * title: string * description: string + * completed: boolean + * }[] + * alerts: { + * id: number + * message: string + * type: 'reminder' | 'notification' + * read: boolean * }[] - * alerts: { id: number; read: boolean }[] + * } + * + * const state: State = { + * todos: [ + * { + * id: 0, + * title: 'Buy groceries', + * description: 'Milk, bread, eggs, and fruits', + * completed: false + * }, + * { + * id: 1, + * title: 'Schedule dentist appointment', + * description: 'Check available slots for next week', + * completed: true + * } + * ], + * alerts: [ + * { + * id: 0, + * message: 'You have an upcoming meeting at 3 PM.', + * type: 'reminder', + * read: false + * }, + * { + * id: 1, + * message: 'New software update available.', + * type: 'notification', + * read: true + * } + * ] * } * * // This: * const structuredSelector = createStructuredSelector( * { - * todos: (state: RootState) => state.todos, - * alerts: (state: RootState) => state.alerts, - * todoById: (state: RootState, id: number) => state.todos[id] + * allTodos: (state: State) => state.todos, + * allAlerts: (state: State) => state.alerts, + * selectedTodo: (state: State, id: number) => state.todos[id] * }, * createSelector * ) @@ -102,15 +137,15 @@ export interface StructuredSelectorCreator { * // Is essentially the same as this: * const selector = createSelector( * [ - * (state: RootState) => state.todos, - * (state: RootState) => state.alerts, - * (state: RootState, id: number) => state.todos[id] + * (state: State) => state.todos, + * (state: State) => state.alerts, + * (state: State, id: number) => state.todos[id] * ], - * (todos, alerts, todoById) => { + * (allTodos, allAlerts, selectedTodo) => { * return { - * todos, - * alerts, - * todoById + * allTodos, + * allAlerts, + * selectedTodo * } * } * ) @@ -143,64 +178,60 @@ export interface StructuredSelectorCreator { MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize >( - inputSelectorsObject: InputSelectorsObject, + selectorMap: InputSelectorsObject, selectorCreator?: CreateSelectorFunction< MemoizeFunction, ArgsMemoizeFunction > ): OutputSelector< ObjectValuesToTuple, - Simplify>, + SelectorsMap, MemoizeFunction, ArgsMemoizeFunction - > & - InterruptRecursion + > + // TODO: Do we need this? + /** + * Second overload + */ + // < + // State, + // Result = State, + // MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + // ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize + // >( + // selectors: { + // [Key in keyof State]: Selector + // }, + // selectorCreator?: CreateSelectorFunction< + // MemoizeFunction, + // ArgsMemoizeFunction + // > + // ): OutputSelector< + // readonly Selector[], + // Result, + // MemoizeFunction, + // ArgsMemoizeFunction + // > } +// Manual definition of state and output arguments /** - * A convenience function that simplifies returning an object - * made up of selector results. + * A convenience function for a common pattern that arises when using Reselect. + * The selector passed to a `connect` decorator often just takes the values of its input selectors + * and maps them to keys in an object. * * @example - * Modern Use Case + * Simple Use Case * ```ts - * import { createSelector, createStructuredSelector } from 'reselect' - * - * interface RootState { - * todos: { - * id: number - * completed: boolean - * title: string - * description: string - * }[] - * alerts: { id: number; read: boolean }[] - * } - * - * // This: - * const structuredSelector = createStructuredSelector( - * { - * todos: (state: RootState) => state.todos, - * alerts: (state: RootState) => state.alerts, - * todoById: (state: RootState, id: number) => state.todos[id] - * }, - * createSelector - * ) + * const selectA = state => state.a + * const selectB = state => state.b * - * // Is essentially the same as this: - * const selector = createSelector( - * [ - * (state: RootState) => state.todos, - * (state: RootState) => state.alerts, - * (state: RootState, id: number) => state.todos[id] - * ], - * (todos, alerts, todoById) => { - * return { - * todos, - * alerts, - * todoById - * } - * } - * ) + * // The result function in the following selector + * // is simply building an object from the input selectors + * const structuredSelector = createSelector(selectA, selectB, (a, b) => ({ + * a, + * b + * })) * ``` * * @see {@link https://github.com/reduxjs/reselect#createstructuredselectorinputselectors-selectorcreator--createselector createStructuredSelector} diff --git a/src/defaultMemoize.ts b/src/defaultMemoize.ts index 716c8a4df..a262e35c3 100644 --- a/src/defaultMemoize.ts +++ b/src/defaultMemoize.ts @@ -1,9 +1,4 @@ -import type { - AnyFunction, - DefaultMemoizeFields, - EqualityFn, - Simplify -} from './types' +import type { AnyFunction, EqualityFn } from './types' // Cache implementation based on Erik Rasmussen's `lru-memoize`: // https://github.com/erikras/lru-memoize @@ -93,9 +88,6 @@ function createLruCache(maxSize: number, equals: EqualityFn): Cache { } /** - * Runs a simple reference equality check. - * What {@linkcode defaultMemoize defaultMemoize} uses by default. - * * @public */ export const defaultEqualityCheck: EqualityFn = (a, b): boolean => { @@ -214,5 +206,5 @@ export function defaultMemoize( cache.clear() } - return memoized as Func & Simplify + return memoized as Func & { clearCache: () => void } } diff --git a/src/index.ts b/src/index.ts index 6a85cb051..c6db78efe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,19 +15,16 @@ export type { DefaultMemoizeOptions } from './defaultMemoize' export type { Combiner, CreateSelectorOptions, - DefaultMemoizeFields, EqualityFn, - ExtractMemoizerFields, GetParamsFromSelectors, GetStateFromSelectors, - MemoizeOptionsFromParameters, + OutputParametricSelector, OutputSelector, OutputSelectorFields, - OverrideMemoizeOptions, + ParametricSelector, Selector, SelectorArray, SelectorResultArray, - StabilityCheckFrequency, - UnknownMemoizer + StabilityCheckFrequency } from './types' export { weakMapMemoize } from './weakMapMemoize' diff --git a/src/types.ts b/src/types.ts index 8a8dca4db..c79719a37 100644 --- a/src/types.ts +++ b/src/types.ts @@ -93,39 +93,36 @@ export interface CreateSelectorOptions< * ```ts * import { createSelector, weakMapMemoize } from 'reselect' * - * const selectItemsByCategory = createSelector( + * const selectTodoById = createSelector( * [ - * (state: RootState) => state.items, - * (state: RootState, category: string) => category + * (state: RootState) => state.todos, + * (state: RootState, id: number) => id * ], - * (items, category) => items.filter(item => item.category === category), + * (todos) => todos[id], * { memoize: weakMapMemoize } * ) * ``` * * @since 5.0.0 */ - memoize?: FallbackIfNever + // If `memoize` is not provided inside the options object, fallback to `MemoizeFunction` which is the original memoize function passed into `createSelectorCreator`. + memoize: FallbackIfNever /** - * The optional memoize function that is used to memoize the arguments - * passed into the output selector generated by `createSelector` - * (e.g., `defaultMemoize` or `weakMapMemoize`). + * The optional memoize function that is used to memoize the arguments passed into the output selector generated by `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). * - * When passed directly into `createSelector`, it overrides the - * `argsMemoize` function initially passed into `createSelectorCreator`. - * If none was initially provided, `defaultMemoize` will be used. + * When passed directly into `createSelector`, it overrides the `argsMemoize` function initially passed into `createSelectorCreator`. If none was initially provided, `defaultMemoize` will be used. * * @example * ```ts * import { createSelector, weakMapMemoize } from 'reselect' * - * const selectItemsByCategory = createSelector( + * const selectTodoById = createSelector( * [ - * (state: RootState) => state.items, - * (state: RootState, category: string) => category + * (state: RootState) => state.todos, + * (state: RootState, id: number) => id * ], - * (items, category) => items.filter(item => item.category === category), + * (todos) => todos[id], * { argsMemoize: weakMapMemoize } * ) * ``` @@ -134,6 +131,9 @@ export interface CreateSelectorOptions< * * @since 5.0.0 */ + // If `argsMemoize` is not provided inside the options object, + // fallback to `ArgsMemoizeFunction` which is the original `argsMemoize` function passed into `createSelectorCreator`. + // If none was passed originally to `createSelectorCreator`, it should fallback to `defaultMemoize`. argsMemoize?: FallbackIfNever< OverrideArgsMemoizeFunction, ArgsMemoizeFunction @@ -145,6 +145,7 @@ export interface CreateSelectorOptions< * * @since 5.0.0 */ + // Should dynamically change to the options argument of `memoize`. memoizeOptions?: OverrideMemoizeOptions< MemoizeFunction, OverrideMemoizeFunction @@ -186,55 +187,19 @@ export type OutputSelectorFields< MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize > = { - /** - * The final function passed to `createSelector`. Otherwise known as the `combiner`. - */ + /** The final function passed to `createSelector`. Otherwise known as the `combiner`.*/ resultFunc: Combiner - - /** - * The memoized version of {@linkcode OutputSelectorFields.resultFunc resultFunc}. - */ + /** The memoized version of {@linkcode OutputSelectorFields.resultFunc resultFunc}. */ memoizedResultFunc: Combiner & ExtractMemoizerFields - - /** - * @Returns The last result calculated by {@linkcode OutputSelectorFields.memoizedResultFunc memoizedResultFunc}. - */ + /** Returns the last result calculated by the output selector. */ lastResult: () => Result - - /** - * The array of the input selectors used by `createSelector` to compose the - * combiner ({@linkcode OutputSelectorFields.memoizedResultFunc memoizedResultFunc}). - */ + /** An array of the input selectors. */ dependencies: InputSelectors - - /** - * Counts the number of times {@linkcode OutputSelectorFields.memoizedResultFunc memoizedResultFunc} has been recalculated. - */ + /** Counts the number of times the output has been recalculated. */ recomputations: () => number - - /** - * Resets the count of {@linkcode OutputSelectorFields.recomputations recomputations} count to 0. - */ + /** Resets the count of `recomputations` count to 0. */ resetRecomputations: () => 0 - - /** - * Counts the number of times the input selectors ({@linkcode OutputSelectorFields.dependencies dependencies}) - * have been recalculated. This is distinct from {@linkcode OutputSelectorFields.recomputations recomputations}, - * which tracks the recalculations of the result function. - * - * @since 5.0.0 - */ - dependencyRecomputations: () => number - - /** - * Resets the count {@linkcode OutputSelectorFields.dependencyRecomputations dependencyRecomputations} - * for the input selectors ({@linkcode OutputSelectorFields.dependencies dependencies}) - * of a memoized selector. - * - * @since 5.0.0 - */ - resetDependencyRecomputations: () => 0 } & Simplify< Required< Pick< @@ -290,6 +255,30 @@ export type Combiner = Distribute< (...resultFuncArgs: SelectorResultArray) => Result > +/** + * A selector that is assumed to have one additional argument, such as + * the props from a React component. + * + * @public + */ +export type ParametricSelector = Selector< + State, + Result, + [Props, ...any] +> + +/** + * A generated selector that is assumed to have one additional argument. + * + * @public + */ +export type OutputParametricSelector = ParametricSelector< + State, + Props, + Result +> & + OutputSelectorFields + /** * A standard function returning true if two values are considered equal. * @@ -322,37 +311,50 @@ export type GetParamsFromSelectors = ArrayTail< MergeParameters > +/* + * ----------------------------------------------------------------------------- + * ----------------------------------------------------------------------------- + * + * Reselect Internal Utility Types + * + * ----------------------------------------------------------------------------- + * ----------------------------------------------------------------------------- + */ + +/** + * Any function with any arguments. + * + * @internal + */ +export type AnyFunction = (...args: any[]) => any + +/** + * Any function with unknown arguments. + * + * @internal + */ +export type UnknownFunction = (...args: unknown[]) => unknown + /** * Any Memoizer function. A memoizer is a function that accepts another function and returns it. * * @template FunctionType - The type of the function that is memoized. * - * @public + * @internal */ export type UnknownMemoizer< FunctionType extends UnknownFunction = UnknownFunction > = (func: FunctionType, ...options: any[]) => FunctionType /** - * Extracts the options type for a memoization function based on its parameters. - * The first parameter of the function is expected to be the function to be memoized, - * followed by options for the memoization process. + * When a generic type parameter is using its default value of `never`, fallback to a different type. * - * @template MemoizeFunction - The type of the memoize function to be checked. + * @template T - Type to be checked. + * @template FallbackTo - Type to fallback to if `T` resolves to `never`. * - * @public + * @internal */ -export type MemoizeOptionsFromParameters< - MemoizeFunction extends UnknownMemoizer -> = - | ( - | NonFunctionType[0]> - | FunctionType[0]> - ) - | ( - | NonFunctionType[number]> - | FunctionType[number]> - )[] +export type FallbackIfNever = IfNever /** * Derive the type of memoize options object based on whether the memoize function itself was overridden. @@ -362,100 +364,66 @@ export type MemoizeOptionsFromParameters< * @template MemoizeFunction - The type of the `memoize` or `argsMemoize` function initially passed into `createSelectorCreator`. * @template OverrideMemoizeFunction - The type of the optional `memoize` or `argsMemoize` function passed directly into `createSelector` which then overrides the original `memoize` or `argsMemoize` function passed into `createSelectorCreator`. * - * @public + * @internal */ export type OverrideMemoizeOptions< MemoizeFunction extends UnknownMemoizer, OverrideMemoizeFunction extends UnknownMemoizer = never > = IfNever< OverrideMemoizeFunction, - Simplify>, - Simplify> + MemoizeOptionsFromParameters, + MemoizeOptionsFromParameters > /** - * Extracts the additional properties or methods that a memoize function attaches to - * the function it memoizes (e.g., `clearCache`). - * - * @template MemoizeFunction - The type of the memoize function to be checked. - * - * @public - */ -export type ExtractMemoizerFields = - Simplify>> - -/** - * Represents the additional properties attached to a function memoized by `reselect`. - * - * `defaultMemoize`, `weakMapMemoize` and `autotrackMemoize` all return these properties. - * - * @see {@linkcode ExtractMemoizerFields ExtractMemoizerFields} - * - * @public - */ -export type DefaultMemoizeFields = { - /** - * Clears the memoization cache associated with a memoized function. - * This method is typically used to reset the state of the cache, allowing - * for the garbage collection of previously memoized results and ensuring - * that future calls to the function recompute the results. - */ - clearCache: () => void -} - -/* - * ----------------------------------------------------------------------------- - * ----------------------------------------------------------------------------- - * - * Reselect Internal Utility Types - * - * ----------------------------------------------------------------------------- - * ----------------------------------------------------------------------------- - */ - -/** - * Any function with any arguments. + * Extracts the non-function part of a type. * - * @internal - */ -export type AnyFunction = (...args: any[]) => any - -/** - * Any function with unknown arguments. + * @template T - The input type to be refined by excluding function types and index signatures. * * @internal */ -export type UnknownFunction = (...args: unknown[]) => unknown +export type NonFunctionType = OmitIndexSignature> /** - * When a generic type parameter is using its default value of `never`, fallback to a different type. + * Extracts the function part of a type. * - * @template T - Type to be checked. - * @template FallbackTo - Type to fallback to if `T` resolves to `never`. + * @template T - The input type to be refined by extracting function types. * * @internal */ -export type FallbackIfNever = IfNever +export type FunctionType = Extract /** - * Extracts the non-function part of a type. + * Extracts the options type for a memoization function based on its parameters. + * The first parameter of the function is expected to be the function to be memoized, + * followed by options for the memoization process. * - * @template T - The input type to be refined by excluding function types and index signatures. + * @template MemoizeFunction - The type of the memoize function to be checked. * * @internal */ -export type NonFunctionType = Simplify< - OmitIndexSignature> -> +export type MemoizeOptionsFromParameters< + MemoizeFunction extends UnknownMemoizer +> = + | ( + | Simplify[0]>> + | FunctionType[0]> + ) + | ( + | Simplify[number]>> + | FunctionType[number]> + )[] /** - * Extracts the function part of a type. + * Extracts the additional fields that a memoize function attaches to + * the function it memoizes (e.g., `clearCache`). * - * @template T - The input type to be refined by extracting function types. + * @template MemoizeFunction - The type of the memoize function to be checked. * * @internal */ -export type FunctionType = Extract +export type ExtractMemoizerFields = + Simplify>> /** * Extracts the return type from all functions as a tuple. @@ -464,7 +432,7 @@ export type FunctionType = Extract */ export type ExtractReturnType = { [Index in keyof FunctionsArray]: FunctionsArray[Index] extends FunctionsArray[number] - ? FallbackIfUnknown, any> + ? ReturnType : never } @@ -498,11 +466,11 @@ export type Distribute = T extends T ? T : never * * @internal */ -export type FirstArrayElement = ArrayType extends readonly [ +export type FirstArrayElement = TArray extends readonly [ unknown, ...unknown[] ] - ? ArrayType[0] + ? TArray[0] : never /** @@ -510,11 +478,11 @@ export type FirstArrayElement = ArrayType extends readonly [ * * @internal */ -export type ArrayTail = ArrayType extends readonly [ +export type ArrayTail = TArray extends readonly [ unknown, - ...infer Tail + ...infer TTail ] - ? Tail + ? TTail : [] /** @@ -596,8 +564,7 @@ export type UnionToIntersection = ) extends // Infer the `Intersection` type since TypeScript represents the positional // arguments of unions of functions as an intersection of the union. (mergedIntersection: infer Intersection) => void - ? // The `& Union` is to allow indexing by the resulting type - Intersection & Union + ? Intersection : never /** @@ -645,42 +612,6 @@ export type ObjectValuesToTuple< ? ObjectValuesToTuple : R -/** - * Create a type that makes the given keys required. - * The remaining keys are kept as is. - * - * @see {@link https://github.com/sindresorhus/type-fest/blob/main/source/set-required.d.ts Source} - * - * @internal - */ -export type SetRequired = Omit< - BaseType, - Keys -> & - Required> - -/** - * An if-else-like type that resolves depending on whether the given type is `unknown`. - * @see {@link https://github.com/sindresorhus/type-fest/blob/main/source/if-unknown.d.ts Source} - * - * @internal - */ -export type IfUnknown = unknown extends T // `T` can be `unknown` or `any` - ? [T] extends [null] // `any` can be `null`, but `unknown` can't be - ? TypeIfNotUnknown - : TypeIfUnknown - : TypeIfNotUnknown - -/** - * When a type is resolves to `unknown`, fallback to a different type. - * - * @template T - Type to be checked. - * @template FallbackTo - Type to fallback to if `T` resolves to `unknown`. - * - * @internal - */ -export type FallbackIfUnknown = IfUnknown - /** * * ----------------------------------------------------------------------------- @@ -786,11 +717,9 @@ export type ExpandFunction = * * @internal */ -export type Simplify = T extends AnyFunction - ? T - : { - [KeyType in keyof T]: T[KeyType] - } & AnyNonNullishValue +export type Simplify = { + [KeyType in keyof T]: T[KeyType] +} & AnyNonNullishValue /** * Fully expand a type, deeply diff --git a/src/utils.ts b/src/utils.ts index 54579edf5..ceabd2356 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -73,7 +73,7 @@ export function assertIsArrayOfFunctions( * @param item - The item to be checked. * @returns An array containing the input item. If the input is already an array, it's returned without modification. */ -export const ensureIsArray = (item: unknown) => { +export const ensureIsArray = (item: T | T[]) => { return Array.isArray(item) ? item : [item] } diff --git a/src/versionedTypes/ts47-mergeParameters.ts b/src/versionedTypes/ts47-mergeParameters.ts index 10092b40a..5be0dfc43 100644 --- a/src/versionedTypes/ts47-mergeParameters.ts +++ b/src/versionedTypes/ts47-mergeParameters.ts @@ -4,114 +4,65 @@ import type { AnyFunction } from '@internal/types' /** - * Represents the longest array within an array of arrays. - * - * @template ArrayOfTuples An array of arrays. - * * @internal */ -type LongestTuple = - ArrayOfTuples extends [infer FirstArray extends unknown[]] - ? FirstArray - : ArrayOfTuples extends [ - infer FirstArray, - ...infer RestArrays extends unknown[][] - ] - ? LongerOfTwo> - : never +type LongestTuple = T extends [ + infer U extends unknown[] +] + ? U + : T extends [infer U, ...infer R extends unknown[][]] + ? MostProperties> + : never /** - * Determines the longer of two array types. - * - * @template ArrayOne First array type. - * @template ArrayTwo Second array type. - * * @internal */ -type LongerOfTwo = keyof ArrayTwo extends keyof ArrayOne - ? ArrayOne - : ArrayTwo +type MostProperties = keyof U extends keyof T ? T : U /** - * Extracts the element at a specific index in an array. - * - * @template ArrayType The array type. - * @template Index The index type. - * * @internal */ -type ElementAt< - ArrayType extends unknown[], - Index extends PropertyKey -> = Index extends keyof ArrayType ? ArrayType[Index] : unknown +type ElementAt = N extends keyof T + ? T[N] + : unknown /** - * Maps each array in an array of arrays to its element at a given index. - * - * @template ArrayOfTuples An array of arrays. - * @template Index The index to extract from each array. - * * @internal */ -type ElementsAtGivenIndex< - ArrayOfTuples extends readonly unknown[][], - Index extends PropertyKey -> = { - [ArrayIndex in keyof ArrayOfTuples]: ElementAt< - ArrayOfTuples[ArrayIndex], - Index - > +type ElementsAt = { + [K in keyof T]: ElementAt } /** - * Computes the intersection of all types in a tuple. - * - * @template Tuple A tuple of types. - * * @internal */ -type Intersect = Tuple extends [] +type Intersect = T extends [] ? unknown - : Tuple extends [infer Head, ...infer Tail] - ? Head & Intersect - : Tuple[number] + : T extends [infer H, ...infer T] + ? H & Intersect + : T[number] /** - * Merges a tuple of arrays into a single tuple, intersecting types at each index. - * - * @template ArrayOfTuples An array of tuples. - * @template LongestArray The longest array in ArrayOfTuples. - * * @internal */ type MergeTuples< - ArrayOfTuples extends readonly unknown[][], - LongestArray extends unknown[] = LongestTuple + T extends readonly unknown[][], + L extends unknown[] = LongestTuple > = { - [Index in keyof LongestArray]: Intersect< - ElementsAtGivenIndex - > + [K in keyof L]: Intersect> } /** - * Extracts the parameter types from a tuple of functions. - * - * @template FunctionsArray An array of function types. - * * @internal */ -type ExtractParameters = { - [Index in keyof FunctionsArray]: Parameters +type ExtractParameters = { + [K in keyof T]: Parameters } /** - * Merges the parameters of a tuple of functions into a single tuple. - * - * @template FunctionsArray An array of function types. - * * @internal */ -export type MergeParameters = - '0' extends keyof FunctionsArray - ? MergeTuples> - : Parameters +export type MergeParameters = + '0' extends keyof T + ? MergeTuples> + : Parameters diff --git a/src/weakMapMemoize.ts b/src/weakMapMemoize.ts index 33e783d9f..5311c9078 100644 --- a/src/weakMapMemoize.ts +++ b/src/weakMapMemoize.ts @@ -1,46 +1,22 @@ // Original source: // - https://github.com/facebook/react/blob/0b974418c9a56f6c560298560265dcf4b65784bc/packages/react/src/ReactCache.js -import type { AnyFunction, DefaultMemoizeFields, Simplify } from './types' +import type { AnyFunction } from './types' const UNTERMINATED = 0 const TERMINATED = 1 interface UnterminatedCacheNode { - /** - * Status, represents whether the cached computation returned a value or threw an error. - */ s: 0 - /** - * Value, either the cached result or an error, depending on status. - */ v: void - /** - * Object cache, a `WeakMap` where non-primitive arguments are stored. - */ o: null | WeakMap> - /** - * Primitive cache, a regular Map where primitive arguments are stored. - */ p: null | Map> } interface TerminatedCacheNode { - /** - * Status, represents whether the cached computation returned a value or threw an error. - */ s: 1 - /** - * Value, either the cached result or an error, depending on status. - */ v: T - /** - * Object cache, a `WeakMap` where non-primitive arguments are stored. - */ o: null | WeakMap> - /** - * Primitive cache, a regular `Map` where primitive arguments are stored. - */ p: null | Map> } @@ -48,21 +24,21 @@ type CacheNode = TerminatedCacheNode | UnterminatedCacheNode function createCacheNode(): CacheNode { return { - s: UNTERMINATED, - v: undefined, - o: null, - p: null + s: UNTERMINATED, // status, represents whether the cached computation returned a value or threw an error + v: undefined, // value, either the cached result or an error, depending on s + o: null, // object cache, a WeakMap where non-primitive arguments are stored + p: null // primitive cache, a regular Map where primitive arguments are stored. } } /** * 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 selectors). - * This allows `weakMapMemoize` to have an effectively infinite cache size. + * 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. * - * __Design Tradeoffs for `weakMapMemoize`:__ + * __Design Tradeoffs for `weakmapMemoize`:__ * - Pros: * - 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 `WeakMap`s. @@ -71,7 +47,7 @@ function createCacheNode(): CacheNode { * They're based on strict reference equality. * - It's roughly the same speed as `defaultMemoize`, although likely a fraction slower. * - * __Use Cases for `weakMapMemoize`:__ + * __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 @@ -87,20 +63,13 @@ function createCacheNode(): CacheNode { * ```ts * import { createSelector, weakMapMemoize } from 'reselect' * - * interface RootState { - * items: { id: number; category: string; name: string }[] - * } - * - * const selectItemsByCategory = createSelector( + * const selectTodoById = createSelector( * [ - * (state: RootState) => state.items, - * (state: RootState, category: string) => category + * (state: RootState) => state.todos, + * (state: RootState, id: number) => id * ], - * (items, category) => items.filter(item => item.category === category), - * { - * memoize: weakMapMemoize, - * argsMemoize: weakMapMemoize - * } + * (todos) => todos[id], + * { memoize: weakMapMemoize } * ) * ``` * @@ -109,14 +78,14 @@ function createCacheNode(): CacheNode { * ```ts * import { createSelectorCreator, weakMapMemoize } from 'reselect' * - * const createSelectorWeakMap = createSelectorCreator({ memoize: weakMapMemoize, argsMemoize: weakMapMemoize }) + * const createSelectorWeakmap = createSelectorCreator(weakMapMemoize) * - * const selectItemsByCategory = createSelectorWeakMap( + * const selectTodoById = createSelectorWeakmap( * [ - * (state: RootState) => state.items, - * (state: RootState, category: string) => category + * (state: RootState) => state.todos, + * (state: RootState, id: number) => id * ], - * (items, category) => items.filter(item => item.category === category) + * (todos) => todos[id] * ) * ``` * @@ -127,12 +96,14 @@ function createCacheNode(): CacheNode { * @experimental */ export function weakMapMemoize(func: Func) { + // we reference arguments instead of spreading them for performance reasons + let fnNode = createCacheNode() function memoized() { let cacheNode = fnNode - const { length } = arguments - for (let i = 0, l = length; i < l; i++) { + + for (let i = 0, l = arguments.length; i < l; i++) { const arg = arguments[i] if ( typeof arg === 'function' || @@ -180,5 +151,5 @@ export function weakMapMemoize(func: Func) { fnNode = createCacheNode() } - return memoized as Func & Simplify + return memoized as Func & { clearCache: () => void } } diff --git a/test/autotrackMemoize.spec.ts b/test/autotrackMemoize.spec.ts index 726f18d16..12d4a14ce 100644 --- a/test/autotrackMemoize.spec.ts +++ b/test/autotrackMemoize.spec.ts @@ -1,10 +1,7 @@ -import { - createSelectorCreator, - unstable_autotrackMemoize as autotrackMemoize -} from 'reselect' +import { createSelectorCreator, unstable_autotrackMemoize as autotrackMemoize } from 'reselect' // Construct 1E6 states for perf test outside of the perf test so as to not change the execute time of the test function -const numOfStates = 1_000_000 +const numOfStates = 1000000 interface StateA { a: number } diff --git a/test/benchmarks/orderOfExecution.bench.ts b/test/benchmarks/orderOfExecution.bench.ts deleted file mode 100644 index a4f307e8b..000000000 --- a/test/benchmarks/orderOfExecution.bench.ts +++ /dev/null @@ -1,166 +0,0 @@ -import type { OutputSelector, Selector } from 'reselect' -import { createSelector, defaultMemoize } from 'reselect' -import type { Options } from 'tinybench' -import { bench } from 'vitest' -import type { RootState } from '../testUtils' -import { - logRecomputations, - setFunctionNames, - setupStore, - toggleCompleted -} from '../testUtils' - -describe('less in input selectors vs more in input selectors', () => { - const store = setupStore() - const arr = Array.from({ length: 1_000_000 }, (e, i) => i) - const runSelector = (selector: Selector) => { - arr.forEach((e, i) => { - selector(store.getState(), 0) - }) - arr.forEach((e, i) => { - selector(store.getState(), 0) - }) - } - - const selectorGood = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.find(todo => todo.id === id)?.completed - ) - const selectorBad = createSelector( - [ - (state: RootState, id: number) => state.todos.find(todo => todo.id === id) - ], - todo => todo?.completed - ) - - let called = 0 - const nonMemoized = (state: RootState, id: number) => { - called++ - return state.todos.find(todo => todo.id === id)?.completed - } - const commonOptions: Options = { - iterations: 10, - time: 0 - } - setFunctionNames({ selectorGood, selectorBad, nonMemoized }) - const createOptions = ( - selector: S, - commonOptions: Options = {} - ) => { - const options: Options = { - setup: (task, mode) => { - if (mode === 'warmup') return - task.opts = { - beforeEach: () => { - store.dispatch(toggleCompleted(1)) - }, - afterAll: () => { - logRecomputations(selector) - } - } - } - } - return { ...commonOptions, ...options } - } - bench( - selectorGood, - () => { - selectorGood(store.getState(), 0) - }, - createOptions(selectorGood, commonOptions) - ) - bench( - selectorBad, - () => { - selectorBad(store.getState(), 0) - }, - createOptions(selectorBad, commonOptions) - ) - bench( - nonMemoized, - () => { - nonMemoized(store.getState(), 0) - }, - { - ...commonOptions, - setup: (task, mode) => { - if (mode === 'warmup') { - called = 0 - return - } - task.opts = { - beforeEach: () => { - store.dispatch(toggleCompleted(1)) - }, - afterAll: () => { - console.log(`${nonMemoized.name} called:`, called, `time(s)`) - } - } - } - } - ) -}) - -describe('using standalone memoization methods vs createSelector', () => { - const store = setupStore() - const commonOptions: Options = { - iterations: 10, - time: 0 - } - const fieldAccessor = createSelector( - [(state: RootState) => state.users], - users => users.appSettings - ) - let called = 0 - const fieldAccessor1 = defaultMemoize((state: RootState) => { - called++ - return state.users.appSettings - }) - setFunctionNames({ fieldAccessor, fieldAccessor1 }) - const createOptions = ( - selector: S, - commonOptions: Options = {} - ) => { - const options: Options = { - setup: (task, mode) => { - if (mode === 'warmup') return - task.opts = { - beforeEach: () => { - store.dispatch(toggleCompleted(1)) - }, - afterAll: () => { - logRecomputations(selector) - } - } - } - } - return { ...commonOptions, ...options } - } - bench( - fieldAccessor, - () => { - fieldAccessor(store.getState()) - }, - createOptions(fieldAccessor, commonOptions) - ) - bench( - fieldAccessor1, - () => { - fieldAccessor1(store.getState()) - }, - { - ...commonOptions, - setup: (task, mode) => { - if (mode === 'warmup') return - task.opts = { - beforeEach: () => { - store.dispatch(toggleCompleted(1)) - }, - afterAll: () => { - console.log(fieldAccessor1.name, called) - } - } - } - } - ) -}) diff --git a/test/benchmarks/reselect.bench.ts b/test/benchmarks/reselect.bench.ts deleted file mode 100644 index 6cc95ce7f..000000000 --- a/test/benchmarks/reselect.bench.ts +++ /dev/null @@ -1,308 +0,0 @@ -import { - createSelector, - unstable_autotrackMemoize as autotrackMemoize, - weakMapMemoize -} from 'reselect' -import type { Options } from 'tinybench' -import { bench } from 'vitest' -import type { RootState } from '../testUtils' -import { setFunctionNames, setupStore } from '../testUtils' - -describe('general benchmark', () => { - const commonOptions: Options = { - iterations: 10, - time: 0 - } - const store = setupStore() - const state = store.getState() - const selectorDefault = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(({ id }) => id) - ) - const selectorAutotrack = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(({ id }) => id), - { memoize: autotrackMemoize } - ) - const selectorWeakMap = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(({ id }) => id), - { memoize: weakMapMemoize } - ) - const selectorArgsAutotrack = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(({ id }) => id), - { argsMemoize: autotrackMemoize } - ) - const nonMemoizedSelector = (state: RootState) => { - return state.todos.map(({ id }) => id) - } - const selectorArgsWeakMap = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(({ id }) => id), - { argsMemoize: weakMapMemoize } - ) - const parametricSelector = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos[id] - ) - const parametricSelectorWeakMapArgs = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos[id], - { argsMemoize: weakMapMemoize } - ) - setFunctionNames({ - selectorDefault, - selectorAutotrack, - selectorWeakMap, - selectorArgsAutotrack, - nonMemoizedSelector, - selectorArgsWeakMap, - parametricSelector, - parametricSelectorWeakMapArgs - }) - bench( - selectorDefault, - () => { - selectorDefault(state) - }, - commonOptions - ) - bench( - selectorAutotrack, - () => { - selectorAutotrack(state) - }, - commonOptions - ) - bench( - selectorWeakMap, - () => { - selectorWeakMap(state) - }, - commonOptions - ) - bench( - selectorArgsAutotrack, - () => { - selectorArgsAutotrack(state) - }, - commonOptions - ) - bench( - selectorArgsWeakMap, - () => { - selectorArgsWeakMap(state) - }, - commonOptions - ) - bench( - nonMemoizedSelector, - () => { - nonMemoizedSelector(state) - }, - commonOptions - ) - bench( - parametricSelector, - () => { - parametricSelector(state, 0) - }, - commonOptions - ) - bench( - parametricSelectorWeakMapArgs, - () => { - parametricSelectorWeakMapArgs(state, 0) - }, - commonOptions - ) -}) - -describe('for loops', () => { - const commonOptions: Options = { - iterations: 10, - time: 0 - } - const store = setupStore() - const state = store.getState() - const { todos } = state - const { length } = todos - bench( - 'for loop length not cached', - () => { - for (let i = 0; i < todos.length; i++) { - // - todos[i].completed - todos[i].id - } - }, - commonOptions - ) - bench( - 'for loop length cached', - () => { - for (let i = 0; i < length; i++) { - // - todos[i].completed - todos[i].id - } - }, - commonOptions - ) - bench( - 'for loop length and arg cached', - () => { - for (let i = 0; i < length; i++) { - // - const arg = todos[i] - arg.completed - arg.id - } - }, - commonOptions - ) -}) - -describe.todo('nested field access', () => { - const commonOptions: Options = { - iterations: 10, - time: 0 - } - const store = setupStore() - const state = store.getState() - const selectorDefault = createSelector( - (state: RootState) => state.users, - users => users.user.details.preferences.notifications.push.frequency - ) - const selectorDefault1 = createSelector( - (state: RootState) => state.users.user, - user => user.details.preferences.notifications.push.frequency - ) - const nonMemoizedSelector = (state: RootState) => - state.users.user.details.preferences.notifications.push.frequency - bench( - 'selectorDefault', - () => { - selectorDefault(state) - }, - commonOptions - ) - bench( - 'nonMemoizedSelector', - () => { - nonMemoizedSelector(state) - }, - commonOptions - ) - bench( - 'selectorDefault1', - () => { - selectorDefault1(state) - }, - commonOptions - ) -}) - -describe.todo('simple field access', () => { - const commonOptions: Options = { - iterations: 10, - time: 0 - } - const store = setupStore() - const state = store.getState() - const selectorDefault = createSelector( - (state: RootState) => state.users, - users => users.user.details.preferences.notifications.push.frequency - ) - const selectorDefault1 = createSelector( - (state: RootState) => state.users.user, - user => user.details.preferences.notifications.push.frequency - ) - const selectorDefault2 = createSelector( - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - users => users.user.details.preferences.notifications.push.frequency - ) - const nonMemoizedSelector = (state: RootState) => - state.users.user.details.preferences.notifications.push.frequency - bench( - 'selectorDefault', - () => { - selectorDefault(state) - }, - commonOptions - ) - bench( - 'nonMemoizedSelector', - () => { - nonMemoizedSelector(state) - }, - commonOptions - ) - bench( - 'selectorDefault1', - () => { - selectorDefault1(state) - }, - commonOptions - ) - bench( - 'selectorDefault2', - () => { - selectorDefault2(state) - }, - commonOptions - ) -}) - -describe.todo('field accessors', () => { - const commonOptions: Options = { - iterations: 10, - time: 0 - } - const store = setupStore() - const selectorDefault = createSelector( - [(state: RootState) => state.users], - users => users.appSettings - ) - const nonMemoizedSelector = (state: RootState) => state.users.appSettings - setFunctionNames({ selectorDefault, nonMemoizedSelector }) - bench( - selectorDefault, - () => { - selectorDefault(store.getState()) - }, - { ...commonOptions } - ) - bench( - nonMemoizedSelector, - () => { - nonMemoizedSelector(store.getState()) - }, - { ...commonOptions } - ) -}) diff --git a/test/benchmarks/weakMapMemoize.bench.ts b/test/benchmarks/weakMapMemoize.bench.ts deleted file mode 100644 index 8d705c477..000000000 --- a/test/benchmarks/weakMapMemoize.bench.ts +++ /dev/null @@ -1,337 +0,0 @@ -import type { OutputSelector, Selector } from 'reselect' -import { - createSelector, - unstable_autotrackMemoize as autotrackMemoize, - weakMapMemoize -} from 'reselect' -import { bench } from 'vitest' -import type { RootState } from '../testUtils' -import { - logRecomputations, - resetSelector, - setFunctionNames, - setupStore -} from '../testUtils' - -import type { Options } from 'tinybench' - -describe('weakMapMemoize vs others', () => { - const store = setupStore() - const state = store.getState() - const arr = Array.from({ length: 30 }, (e, i) => i) - const commonOptions: Options = { - iterations: 10, - time: 0 - } - const selectorDefault = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.map(todo => todo.id === id) - ) - const selectorDefaultWithCacheSize = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.map(todo => todo.id === id), - { memoizeOptions: { maxSize: 30 } } - ) - const selectorDefaultWithArgsCacheSize = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.map(todo => todo.id === id), - { argsMemoizeOptions: { maxSize: 30 } } - ) - const selectorDefaultWithBothCacheSize = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.map(todo => todo.id === id), - { memoizeOptions: { maxSize: 30 }, argsMemoizeOptions: { maxSize: 30 } } - ) - const selectorWeakMap = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - (todos, id) => todos.map(todo => todo.id === id), - { memoize: weakMapMemoize } - ) - const selectorAutotrack = createSelector( - (state: RootState) => state.todos, - (state: RootState, id: number) => id, - (todos, id) => todos.map(todo => todo.id === id), - { memoize: autotrackMemoize } - ) - const selectorArgsAutotrack = createSelector( - (state: RootState) => state.todos, - (state: RootState, id: number) => id, - (todos, id) => todos.map(todo => todo.id === id), - { argsMemoize: autotrackMemoize } - ) - const selectorBothAutotrack = createSelector( - (state: RootState) => state.todos, - (state: RootState, id: number) => id, - (todos, id) => todos.map(todo => todo.id === id), - { argsMemoize: autotrackMemoize, memoize: autotrackMemoize } - ) - const selectorArgsWeakMap = createSelector( - (state: RootState) => state.todos, - (state: RootState, id: number) => id, - (todos, id) => todos.map(todo => todo.id === id), - { argsMemoize: weakMapMemoize } - ) - const selectorBothWeakMap = createSelector( - (state: RootState) => state.todos, - (state: RootState, id: number) => id, - (todos, id) => todos.map(todo => todo.id === id), - { argsMemoize: weakMapMemoize, memoize: weakMapMemoize } - ) - const nonMemoizedSelector = (state: RootState, id: number) => { - return state.todos.map(todo => todo.id === id) - } - setFunctionNames({ - selectorDefault, - selectorDefaultWithCacheSize, - selectorDefaultWithArgsCacheSize, - selectorDefaultWithBothCacheSize, - selectorWeakMap, - selectorArgsWeakMap, - selectorBothWeakMap, - selectorAutotrack, - selectorArgsAutotrack, - selectorBothAutotrack, - nonMemoizedSelector - }) - const runSelector = (selector: S) => { - arr.forEach((e, i) => { - selector(state, e) - }) - arr.forEach((e, i) => { - selector(state, e) - }) - } - - const createOptions = ( - selector: S, - commonOptions: Options = {} - ) => { - const options: Options = { - setup: (task, mode) => { - if (mode === 'warmup') return - resetSelector(selector) - task.opts = { - afterAll: () => { - logRecomputations(selector) - } - } - } - } - return { ...commonOptions, ...options } - } - bench( - selectorDefault, - () => { - runSelector(selectorDefault) - }, - createOptions(selectorDefault, commonOptions) - ) - bench( - selectorDefaultWithCacheSize, - () => { - runSelector(selectorDefaultWithCacheSize) - }, - createOptions(selectorDefaultWithCacheSize, commonOptions) - - ) - bench( - selectorDefaultWithArgsCacheSize, - () => { - runSelector(selectorDefaultWithArgsCacheSize) - }, - createOptions(selectorDefaultWithArgsCacheSize, commonOptions) - - ) - bench( - selectorDefaultWithBothCacheSize, - () => { - runSelector(selectorDefaultWithBothCacheSize) - }, - createOptions(selectorDefaultWithBothCacheSize, commonOptions) - - ) - bench( - selectorWeakMap, - () => { - runSelector(selectorWeakMap) - }, - createOptions(selectorWeakMap, commonOptions) - - ) - bench( - selectorArgsWeakMap, - () => { - runSelector(selectorArgsWeakMap) - }, - createOptions(selectorArgsWeakMap, commonOptions) - - ) - bench( - selectorBothWeakMap, - () => { - runSelector(selectorBothWeakMap) - }, - createOptions(selectorBothWeakMap, commonOptions) - - ) - bench( - selectorAutotrack, - () => { - runSelector(selectorAutotrack) - }, - createOptions(selectorAutotrack, commonOptions) - - ) - bench( - selectorArgsAutotrack, - () => { - runSelector(selectorArgsAutotrack) - }, - createOptions(selectorArgsAutotrack, commonOptions) - - ) - bench( - selectorBothAutotrack, - () => { - runSelector(selectorBothAutotrack) - }, - createOptions(selectorBothAutotrack, commonOptions) - - ) - bench( - nonMemoizedSelector, - () => { - runSelector(nonMemoizedSelector) - }, - { ...commonOptions } - ) -}) - -describe('weakMapMemoize simple examples', () => { - const store = setupStore() - const state = store.getState() - const arr = Array.from({ length: 30 }, (e, i) => i) - const commonOptions: Options = { - warmupIterations: 0, - warmupTime: 0, - iterations: 1, - time: 0 - } - const selectorDefault = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(({ id }) => id) - ) - const selectorWeakMap = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(({ id }) => id), - { argsMemoize: weakMapMemoize } - ) - const selectorAutotrack = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(({ id }) => id), - { memoize: autotrackMemoize } - ) - - setFunctionNames({ - selectorDefault, - selectorWeakMap, - selectorAutotrack - }) - - const createOptions = (selector: S) => { - const options: Options = { - setup: (task, mode) => { - if (mode === 'warmup') return - resetSelector(selector) - task.opts = { - afterAll: () => { - logRecomputations(selector) - } - } - } - } - return { ...commonOptions, ...options } - } - - bench( - selectorDefault, - () => { - selectorDefault(store.getState()) - }, - createOptions(selectorDefault) - ) - bench( - selectorWeakMap, - () => { - selectorWeakMap(store.getState()) - }, - createOptions(selectorWeakMap) - ) - bench( - selectorAutotrack, - () => { - selectorAutotrack(store.getState()) - }, - createOptions(selectorAutotrack) - ) -}) - -describe.skip('weakMapMemoize vs defaultMemoize memory leak', () => { - const store = setupStore() - const state = store.getState() - const arr = Array.from({ length: 2_000_000 }, (e, i) => i) - const commonOptions: Options = { - warmupIterations: 0, - warmupTime: 0, - iterations: 1, - time: 0 - } - const selectorDefault = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - todos => todos.map(({ id }) => id) - ) - const selectorWeakMap = createSelector( - [(state: RootState) => state.todos, (state: RootState, id: number) => id], - todos => todos.map(({ id }) => id), - { argsMemoize: weakMapMemoize, memoize: weakMapMemoize } - ) - const runSelector = (selector: S) => { - arr.forEach((e, i) => { - selector(state, e) - }) - arr.forEach((e, i) => { - selector(state, e) - }) - } - setFunctionNames({ selectorDefault, selectorWeakMap }) - const createOptions = ( - selector: S, - commonOptions: Options = {} - ) => { - const options: Options = { - setup: (task, mode) => { - if (mode === 'warmup') return - task.opts = { - afterAll: () => { - logRecomputations(selector) - } - } - } - } - return { ...commonOptions, ...options } - } - bench( - selectorDefault, - () => { - runSelector(selectorDefault) - }, - createOptions(selectorDefault, commonOptions) - ) - bench( - selectorWeakMap, - () => { - runSelector(selectorWeakMap) - }, - createOptions(selectorWeakMap, commonOptions) - ) -}) diff --git a/test/createStructuredSelector.spec.ts b/test/createStructuredSelector.spec.ts index 56cc78013..058c9aba5 100644 --- a/test/createStructuredSelector.spec.ts +++ b/test/createStructuredSelector.spec.ts @@ -128,45 +128,4 @@ describe('structured selector created with createStructuredSel ) } ) - - localTest( - 'structured selector invalid args can throw runtime errors', - ({ state }) => { - const structuredSelector = createStructuredSelector( - { - allTodos: (state: RootState) => state.todos, - allAlerts: (state: RootState) => state.alerts, - selectedTodo: ( - state: RootState, - id: number, - field: keyof RootState['todos'][number] - ) => state.todos[id][field] - }, - createSelector - ) - const selector = createSelector( - [ - (state: RootState) => state.todos, - (state: RootState) => state.alerts, - ( - state: RootState, - id: number, - field: keyof RootState['todos'][number] - ) => state.todos[id][field] - ], - (allTodos, allAlerts, selectedTodo) => { - return { - allTodos, - allAlerts, - selectedTodo - } - } - ) - // These two cases are the same. - // @ts-expect-error - expect(() => structuredSelector(state)).toThrowError(TypeError) - // @ts-expect-error - expect(() => selector(state)).toThrowError(TypeError) - } - ) }) diff --git a/test/reselect.bench.ts b/test/reselect.bench.ts new file mode 100644 index 000000000..e69de29bb diff --git a/test/reselect.spec.ts b/test/reselect.spec.ts index e9576fa1c..dfb4abfba 100644 --- a/test/reselect.spec.ts +++ b/test/reselect.spec.ts @@ -11,11 +11,11 @@ import { } from 'reselect' import type { OutputSelector, OutputSelectorFields } from 'reselect' -import type { RootState } from './testUtils' -import { addTodo, deepClone, localTest, toggleCompleted } from './testUtils' +import type { LocalTestContext, RootState } from './testUtils' +import { addTodo, deepClone, setupStore, toggleCompleted } from './testUtils' // Construct 1E6 states for perf test outside of the perf test so as to not change the execute time of the test function -const numOfStates = 1_000_000 +const numOfStates = 1000000 interface StateA { a: number } @@ -395,37 +395,49 @@ describe('Customizing selectors', () => { expect(memoizer3Calls).toBeGreaterThan(0) }) - localTest.todo( - 'Test order of execution in a selector', - ({ store, state }) => { - // original options untouched. - const selectorOriginal = createSelector( - (state: RootState) => state.todos, - todos => todos.map(({ id }) => id), - { - inputStabilityCheck: 'always', - memoizeOptions: { - equalityCheck: (a, b) => false, - resultEqualityCheck: (a, b) => false - } - } - ) - selectorOriginal(deepClone(state)) - selectorOriginal(deepClone(state)) - const selectorDefaultParametric = createSelector( - [ - (state: RootState, id: number) => id, - (state: RootState) => state.todos - ], - (id, todos) => todos.filter(todo => todo.id === id) - ) - selectorDefaultParametric(state, 1) - selectorDefaultParametric(state, 1) + test.todo('Test order of execution in a selector', () => { + interface State { + todos: { + id: number + completed: boolean + }[] + } + const state: State = { + todos: [ + { id: 0, completed: false }, + { id: 1, completed: false } + ] } - ) + // original options untouched. + const selectorOriginal = createSelector( + (state: State) => state.todos, + todos => todos.map(({ id }) => id), + { + inputStabilityCheck: 'always', + memoizeOptions: { + equalityCheck: (a, b) => false, + resultEqualityCheck: (a, b) => false + } + } + ) + selectorOriginal(deepClone(state)) + selectorOriginal(deepClone(state)) + const selectorDefaultParametric = createSelector( + [(state: State, id: number) => id, (state: State) => state.todos], + (id, todos) => todos.filter(todo => todo.id === id) + ) + selectorDefaultParametric(state, 1) + selectorDefaultParametric(state, 1) + }) }) -describe('argsMemoize and memoize', () => { +describe('argsMemoize and memoize', localTest => { + beforeEach(context => { + const store = setupStore() + context.store = store + context.state = store.getState() + }) + localTest('passing memoize directly to createSelector', ({ store }) => { const state = store.getState() const selectorDefault = createSelector( @@ -458,9 +470,7 @@ describe('argsMemoize and memoize', () => { 'lastResult', 'dependencies', 'recomputations', - 'resetRecomputations', - 'dependencyRecomputations', - 'resetDependencyRecomputations' + 'resetRecomputations' ] const memoizerFields: Exclude< keyof OutputSelector, @@ -481,9 +491,7 @@ describe('argsMemoize and memoize', () => { 'lastResult' in selector && 'dependencies' in selector && 'recomputations' in selector && - 'dependencyRecomputations' in selector && 'resetRecomputations' in selector && - 'resetDependencyRecomputations' in selector && 'memoize' in selector && 'argsMemoize' in selector && typeof selector.resultFunc === 'function' && @@ -491,9 +499,7 @@ describe('argsMemoize and memoize', () => { typeof selector.lastResult === 'function' && Array.isArray(selector.dependencies) && typeof selector.recomputations === 'function' && - typeof selector.dependencyRecomputations === 'function' && typeof selector.resetRecomputations === 'function' && - typeof selector.resetDependencyRecomputations === 'function' && typeof selector.memoize === 'function' && typeof selector.argsMemoize === 'function' && selector.dependencies.length >= 1 && @@ -523,9 +529,7 @@ describe('argsMemoize and memoize', () => { expect(selectorDefault.lastResult).to.be.a('function') expect(selectorDefault.dependencies).to.be.an('array').that.is.not.empty expect(selectorDefault.recomputations).to.be.a('function') - expect(selectorDefault.dependencyRecomputations).to.be.a('function') expect(selectorDefault.resetRecomputations).to.be.a('function') - expect(selectorDefault.resetDependencyRecomputations).to.be.a('function') expect(selectorDefault.memoize).to.be.a('function') expect(selectorDefault.argsMemoize).to.be.a('function') expect(selectorDefault.clearCache).to.be.a('function') @@ -535,9 +539,7 @@ describe('argsMemoize and memoize', () => { expect(selectorAutotrack.recomputations()).toBe(0) expect(selectorDefault(state)).toStrictEqual(selectorAutotrack(state)) expect(selectorDefault.recomputations()).toBe(1) - expect(selectorDefault.dependencyRecomputations()).toBe(1) expect(selectorAutotrack.recomputations()).toBe(1) - expect(selectorAutotrack.dependencyRecomputations()).toBe(1) // flipping completed flag does not cause the autotrack memoizer to re-run. store.dispatch(toggleCompleted(0)) selectorDefault(store.getState()) @@ -550,18 +552,14 @@ describe('argsMemoize and memoize', () => { const defaultSelectorLastResult2 = selectorDefault.lastResult() const autotrackSelectorLastResult2 = selectorAutotrack.lastResult() expect(selectorDefault.recomputations()).toBe(3) - expect(selectorDefault.dependencyRecomputations()).toBe(3) expect(selectorAutotrack.recomputations()).toBe(1) - expect(selectorAutotrack.dependencyRecomputations()).toBe(3) for (let i = 0; i < 10; i++) { store.dispatch(toggleCompleted(0)) selectorDefault(store.getState()) selectorAutotrack(store.getState()) } expect(selectorDefault.recomputations()).toBe(13) - expect(selectorDefault.dependencyRecomputations()).toBe(13) expect(selectorAutotrack.recomputations()).toBe(1) - expect(selectorAutotrack.dependencyRecomputations()).toBe(13) expect(autotrackSelectorLastResult1).toBe(autotrackSelectorLastResult2) expect(defaultSelectorLastResult1).not.toBe(defaultSelectorLastResult2) // Default memoize does not preserve referential equality but autotrack does. expect(defaultSelectorLastResult1).toStrictEqual(defaultSelectorLastResult2) @@ -573,7 +571,6 @@ describe('argsMemoize and memoize', () => { ) selectorAutotrack(store.getState()) expect(selectorAutotrack.recomputations()).toBe(2) - expect(selectorAutotrack.dependencyRecomputations()).toBe(14) }) localTest('passing argsMemoize directly to createSelector', ({ store }) => { @@ -596,8 +593,6 @@ describe('argsMemoize and memoize', () => { ) expect(selectorDefault.recomputations()).toBe(1) expect(selectorAutotrack.recomputations()).toBe(1) - expect(selectorDefault.dependencyRecomputations()).toBe(1) - expect(selectorAutotrack.dependencyRecomputations()).toBe(1) selectorDefault(store.getState()) selectorAutotrack(store.getState()) // toggling the completed flag should force the default memoizer to recalculate but not autotrack. @@ -617,8 +612,6 @@ describe('argsMemoize and memoize', () => { store.dispatch(toggleCompleted(2)) expect(selectorDefault.recomputations()).toBe(4) expect(selectorAutotrack.recomputations()).toBe(1) - expect(selectorDefault.dependencyRecomputations()).toBe(4) - expect(selectorAutotrack.dependencyRecomputations()).toBe(4) selectorDefault(store.getState()) selectorAutotrack(store.getState()) store.dispatch(toggleCompleted(0)) @@ -634,8 +627,6 @@ describe('argsMemoize and memoize', () => { const autotrackSelectorLastResult2 = selectorAutotrack.lastResult() expect(selectorDefault.recomputations()).toBe(6) expect(selectorAutotrack.recomputations()).toBe(1) - expect(selectorDefault.dependencyRecomputations()).toBe(6) - expect(selectorAutotrack.dependencyRecomputations()).toBe(7) expect(autotrackSelectorLastResult1).toBe(autotrackSelectorLastResult2) expect(defaultSelectorLastResult1).not.toBe(defaultSelectorLastResult2) expect(defaultSelectorLastResult1).toStrictEqual(defaultSelectorLastResult2) @@ -649,8 +640,6 @@ describe('argsMemoize and memoize', () => { } expect(selectorAutotrack.recomputations()).toBe(1) expect(selectorDefault.recomputations()).toBe(16) - expect(selectorAutotrack.dependencyRecomputations()).toBe(17) - expect(selectorDefault.dependencyRecomputations()).toBe(16) // original options untouched. const selectorOriginal = createSelector( [(state: RootState) => state.todos], @@ -683,8 +672,6 @@ describe('argsMemoize and memoize', () => { } expect(selectorOverrideArgsMemoize.recomputations()).toBe(1) expect(selectorOriginal.recomputations()).toBe(11) - expect(selectorOverrideArgsMemoize.dependencyRecomputations()).toBe(1) - expect(selectorOriginal.dependencyRecomputations()).toBe(11) const selectorDefaultParametric = createSelector( [(state: RootState, id: number) => id, (state: RootState) => state.todos], (id, todos) => todos.filter(todo => todo.id === id) @@ -692,14 +679,11 @@ describe('argsMemoize and memoize', () => { selectorDefaultParametric(store.getState(), 1) selectorDefaultParametric(store.getState(), 1) expect(selectorDefaultParametric.recomputations()).toBe(1) - expect(selectorDefaultParametric.dependencyRecomputations()).toBe(1) selectorDefaultParametric(store.getState(), 2) selectorDefaultParametric(store.getState(), 1) expect(selectorDefaultParametric.recomputations()).toBe(3) - expect(selectorDefaultParametric.dependencyRecomputations()).toBe(3) selectorDefaultParametric(store.getState(), 2) expect(selectorDefaultParametric.recomputations()).toBe(4) - expect(selectorDefaultParametric.dependencyRecomputations()).toBe(4) const selectorDefaultParametricArgsWeakMap = createSelector( [(state: RootState, id: number) => id, (state: RootState) => state.todos], (id, todos) => todos.filter(todo => todo.id === id), @@ -713,23 +697,14 @@ describe('argsMemoize and memoize', () => { selectorDefaultParametricArgsWeakMap(store.getState(), 1) selectorDefaultParametricArgsWeakMap(store.getState(), 1) expect(selectorDefaultParametricArgsWeakMap.recomputations()).toBe(1) - expect( - selectorDefaultParametricArgsWeakMap.dependencyRecomputations() - ).toBe(1) selectorDefaultParametricArgsWeakMap(store.getState(), 2) selectorDefaultParametricArgsWeakMap(store.getState(), 1) expect(selectorDefaultParametricArgsWeakMap.recomputations()).toBe(2) - expect( - selectorDefaultParametricArgsWeakMap.dependencyRecomputations() - ).toBe(2) selectorDefaultParametricArgsWeakMap(store.getState(), 2) // If we call the selector with 1, then 2, then 1 and back to 2 again, // `defaultMemoize` will recompute a total of 4 times, // but weakMapMemoize will recompute only twice. expect(selectorDefaultParametricArgsWeakMap.recomputations()).toBe(2) - expect( - selectorDefaultParametricArgsWeakMap.dependencyRecomputations() - ).toBe(2) for (let i = 0; i < 10; i++) { selectorDefaultParametricArgsWeakMap(store.getState(), 1) selectorDefaultParametricArgsWeakMap(store.getState(), 2) @@ -738,9 +713,6 @@ describe('argsMemoize and memoize', () => { selectorDefaultParametricArgsWeakMap(store.getState(), 5) } expect(selectorDefaultParametricArgsWeakMap.recomputations()).toBe(5) - expect( - selectorDefaultParametricArgsWeakMap.dependencyRecomputations() - ).toBe(5) for (let i = 0; i < 10; i++) { selectorDefaultParametric(store.getState(), 1) selectorDefaultParametric(store.getState(), 2) @@ -749,7 +721,6 @@ describe('argsMemoize and memoize', () => { selectorDefaultParametric(store.getState(), 5) } expect(selectorDefaultParametric.recomputations()).toBe(54) - expect(selectorDefaultParametric.dependencyRecomputations()).toBe(54) for (let i = 0; i < 10; i++) { selectorDefaultParametricWeakMap(store.getState(), 1) selectorDefaultParametricWeakMap(store.getState(), 2) @@ -758,7 +729,6 @@ describe('argsMemoize and memoize', () => { selectorDefaultParametricWeakMap(store.getState(), 5) } expect(selectorDefaultParametricWeakMap.recomputations()).toBe(5) - expect(selectorDefaultParametricWeakMap.dependencyRecomputations()).toBe(50) }) localTest('passing argsMemoize to createSelectorCreator', ({ store }) => { @@ -805,9 +775,7 @@ describe('argsMemoize and memoize', () => { ]) ).to.be.an('array').that.is.not.empty expect(selectorMicroMemoize.recomputations()).to.be.a('number') - expect(selectorMicroMemoize.dependencyRecomputations()).to.be.a('number') expect(selectorMicroMemoize.resetRecomputations()).toBe(0) - expect(selectorMicroMemoize.resetDependencyRecomputations()).toBe(0) expect(selectorMicroMemoize.resultFunc).to.be.a('function') expect( selectorMicroMemoize.resultFunc([ @@ -871,13 +839,7 @@ describe('argsMemoize and memoize', () => { ]) ).to.be.an('array').that.is.not.empty expect(selectorMicroMemoizeOverridden.recomputations()).to.be.a('number') - expect(selectorMicroMemoizeOverridden.dependencyRecomputations()).to.be.a( - 'number' - ) expect(selectorMicroMemoizeOverridden.resetRecomputations()).toBe(0) - expect(selectorMicroMemoizeOverridden.resetDependencyRecomputations()).toBe( - 0 - ) expect( selectorMicroMemoizeOverridden.resultFunc([ { @@ -952,15 +914,9 @@ describe('argsMemoize and memoize', () => { expect( selectorMicroMemoizeOverrideArgsMemoizeOnly.recomputations() ).to.be.a('number') - expect( - selectorMicroMemoizeOverrideArgsMemoizeOnly.dependencyRecomputations() - ).to.be.a('number') expect( selectorMicroMemoizeOverrideArgsMemoizeOnly.resetRecomputations() ).toBe(0) - expect( - selectorMicroMemoizeOverrideArgsMemoizeOnly.resetDependencyRecomputations() - ).toBe(0) expect( selectorMicroMemoizeOverrideArgsMemoizeOnly.resultFunc([ { @@ -995,9 +951,7 @@ describe('argsMemoize and memoize', () => { 'recomputations', 'resetRecomputations', 'memoize', - 'argsMemoize', - 'dependencyRecomputations', - 'resetDependencyRecomputations' + 'argsMemoize' ]) expect(selectorMicroMemoizeOverrideMemoizeOnly.cache).to.be.an('object') expect(selectorMicroMemoizeOverrideMemoizeOnly.fn).to.be.a('function') @@ -1030,15 +984,9 @@ describe('argsMemoize and memoize', () => { expect(selectorMicroMemoizeOverrideMemoizeOnly.recomputations()).to.be.a( 'number' ) - expect( - selectorMicroMemoizeOverrideMemoizeOnly.dependencyRecomputations() - ).to.be.a('number') expect(selectorMicroMemoizeOverrideMemoizeOnly.resetRecomputations()).toBe( 0 ) - expect( - selectorMicroMemoizeOverrideMemoizeOnly.resetDependencyRecomputations() - ).toBe(0) expect( selectorMicroMemoizeOverrideMemoizeOnly.resultFunc([ { @@ -1051,92 +999,22 @@ describe('argsMemoize and memoize', () => { ).to.be.an('array').that.is.not.empty }) - localTest( - 'pass options object to createSelectorCreator ', - ({ store, state }) => { - const createSelectorMicro = createSelectorCreator({ - memoize: microMemoize, - memoizeOptions: { isEqual: (a, b) => a === b } - }) - const selectorMicro = createSelectorMicro( - [(state: RootState) => state.todos], - todos => todos.map(({ id }) => id) - ) - const selector = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(({ id }) => id) - ) - const selector1 = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(({ id }) => id), - { memoize: weakMapMemoize } - ) - expect(() => - //@ts-expect-error - createSelectorMicro([(state: RootState) => state.todos], 'a') - ).toThrowError( - TypeError( - `createSelector expects an output function after the inputs, but received: [string]` - ) - ) - const selectorDefault = createSelector( - (state: RootState) => state.users, - users => users.user.details.preferences.notifications.push.frequency - ) - const selectorDefault1 = createSelector( - (state: RootState) => state.users.user, - user => user.details.preferences.notifications.push.frequency - ) - let called = 0 - const selectorDefault2 = createSelector( - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => { - called++ - return state.users - }, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState) => state.users, - (state: RootState, id: number) => state.users, - users => { - return users.user.details.preferences.notifications.push.frequency - }, - { inputStabilityCheck: 'never' } + localTest('pass options object to createSelectorCreator ', ({ store }) => { + const createSelectorMicro = createSelectorCreator({ + memoize: microMemoize, + memoizeOptions: { isEqual: (a, b) => a === b } + }) + const selectorMicro = createSelectorMicro( + [(state: RootState) => state.todos], + todos => todos.map(({ id }) => id) + ) + expect(() => + //@ts-expect-error + createSelectorMicro([(state: RootState) => state.todos], 'a') + ).toThrowError( + TypeError( + `createSelector expects an output function after the inputs, but received: [string]` ) - const start = performance.now() - for (let i = 0; i < 10_000_000; i++) { - selectorDefault(state) - } - expect(performance.now() - start).toBeLessThan(1000) - selectorDefault1(state) - const stateBeforeChange = store.getState() - selectorDefault2(store.getState(), 0) - const stateBeforeChange1 = store.getState() - store.dispatch(toggleCompleted(0)) - const stateAfterChange = store.getState() - expect(stateBeforeChange1).not.toBe(stateAfterChange) - expect(stateBeforeChange1.alerts).toBe(stateAfterChange.alerts) - expect(stateBeforeChange1.todos[1]).toBe(stateAfterChange.todos[1]) - expect(stateBeforeChange1).toBe(stateBeforeChange) - expect(stateBeforeChange1.alerts).toBe(stateBeforeChange.alerts) - } - ) + ) + }) }) diff --git a/test/selectorUtils.spec.ts b/test/selectorUtils.spec.ts index 78daee563..2e34b4d87 100644 --- a/test/selectorUtils.spec.ts +++ b/test/selectorUtils.spec.ts @@ -1,5 +1,4 @@ import { createSelector } from 'reselect' -import type { StateA, StateAB } from 'testTypes' describe('createSelector exposed utils', () => { test('resetRecomputations', () => { diff --git a/test/testUtils.ts b/test/testUtils.ts index faeb41447..f0d096ea8 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -1,13 +1,5 @@ import type { PayloadAction } from '@reduxjs/toolkit' import { combineReducers, configureStore, createSlice } from '@reduxjs/toolkit' -import { test } from 'vitest' -import type { - AnyFunction, - OutputSelector, - Selector, - SelectorArray, - Simplify -} from '../src/types' interface Todo { id: number @@ -23,63 +15,6 @@ interface Alert { read: boolean } -interface BillingAddress { - street: string - city: string - state: string - zip: string -} - -interface Address extends BillingAddress { - billing: BillingAddress -} - -interface PushNotification { - enabled: boolean - frequency: string -} - -interface Notifications { - email: boolean - sms: boolean - push: PushNotification -} - -interface Preferences { - newsletter: boolean - notifications: Notifications -} - -interface Login { - lastLogin: string - loginCount: number -} - -interface UserDetails { - name: string - email: string - address: Address - preferences: Preferences -} - -interface User { - id: number - details: UserDetails - status: string - login: Login -} - -interface AppSettings { - theme: string - language: string -} - -interface UserState { - user: User - appSettings: AppSettings -} - -// For long arrays const todoState = [ { id: 0, @@ -125,25 +60,6 @@ const todoState = [ } ] -export const createTodoItem = (id: number) => { - return { - id, - title: `Task ${id}`, - description: `Description for task ${id}`, - completed: false - } -} - -export const pushToTodos = (howMany: number) => { - const { length: todoStateLength } = todoState - const limit = howMany + todoStateLength - for (let i = todoStateLength; i < limit; i++) { - todoState.push(createTodoItem(i)) - } -} - -pushToTodos(200) - const alertState = [ { id: 0, @@ -187,49 +103,6 @@ const alertState = [ } ] -// For nested fields tests -const userState: UserState = { - user: { - id: 0, - details: { - name: 'John Doe', - email: 'john.doe@example.com', - address: { - street: '123 Main St', - city: 'AnyTown', - state: 'CA', - zip: '12345', - billing: { - street: '456 Main St', - city: 'AnyTown', - state: 'CA', - zip: '12345' - } - }, - preferences: { - newsletter: true, - notifications: { - email: true, - sms: false, - push: { - enabled: true, - frequency: 'daily' - } - } - } - }, - status: 'active', - login: { - lastLogin: '2023-04-30T12:34:56Z', - loginCount: 123 - } - }, - appSettings: { - theme: 'dark', - language: 'en-US' - } -} - const todoSlice = createSlice({ name: 'todos', initialState: todoState, @@ -278,13 +151,6 @@ const alertSlice = createSlice({ } }, - toggleRead: (state, action: PayloadAction) => { - const alert = state.find(alert => alert.id === action.payload) - if (alert) { - alert.read = !alert.read - } - }, - addAlert: (state, action: PayloadAction>) => { const newId = state.length > 0 ? state[state.length - 1].id + 1 : 0 state.push({ @@ -299,79 +165,14 @@ const alertSlice = createSlice({ } }) -const userSlice = createSlice({ - name: 'users', - initialState: userState, - reducers: { - setUserName: (state, action: PayloadAction) => { - state.user.details.name = action.payload - }, - - setUserEmail: (state, action: PayloadAction) => { - state.user.details.email = action.payload - }, - - setAppTheme: (state, action: PayloadAction) => { - state.appSettings.theme = action.payload - }, - - updateUserStatus: (state, action: PayloadAction) => { - state.user.status = action.payload - }, - - updateLoginDetails: ( - state, - action: PayloadAction<{ lastLogin: string; loginCount: number }> - ) => { - state.user.login = { ...state.user.login, ...action.payload } - }, - - updateUserAddress: (state, action: PayloadAction
) => { - state.user.details.address = { - ...state.user.details.address, - ...action.payload - } - }, - - updateBillingAddress: (state, action: PayloadAction) => { - state.user.details.address.billing = { - ...state.user.details.address.billing, - ...action.payload - } - }, - - toggleNewsletterSubscription: state => { - state.user.details.preferences.newsletter = - !state.user.details.preferences.newsletter - }, - - setNotificationPreferences: ( - state, - action: PayloadAction - ) => { - state.user.details.preferences.notifications = { - ...state.user.details.preferences.notifications, - ...action.payload - } - }, - - updateAppLanguage: (state, action: PayloadAction) => { - state.appSettings.language = action.payload - } - } -}) - const rootReducer = combineReducers({ [todoSlice.name]: todoSlice.reducer, - [alertSlice.name]: alertSlice.reducer, - [userSlice.name]: userSlice.reducer + [alertSlice.name]: alertSlice.reducer }) -export const setupStore = (preloadedState?: Partial) => { - return configureStore({ reducer: rootReducer, preloadedState }) -} +export const setupStore = () => configureStore({ reducer: rootReducer }) -export type AppStore = Simplify> +export type AppStore = ReturnType export type RootState = ReturnType @@ -380,8 +181,7 @@ export interface LocalTestContext { state: RootState } -export const { markAsRead, addAlert, removeAlert, toggleRead } = - alertSlice.actions +export const { markAsRead, addAlert, removeAlert } = alertSlice.actions export const { toggleCompleted, @@ -391,50 +191,6 @@ export const { clearCompleted } = todoSlice.actions -export const { setUserName, setUserEmail, setAppTheme } = userSlice.actions - // Since Node 16 does not support `structuredClone` export const deepClone = (object: T): T => JSON.parse(JSON.stringify(object)) - -export const setFunctionName = (func: AnyFunction, name: string) => { - Object.defineProperty(func, 'name', { value: name }) -} - -export const setFunctionNames = (funcObject: Record) => { - Object.entries(funcObject).forEach(([key, value]) => - setFunctionName(value, key) - ) -} - -const store = setupStore() -const state = store.getState() - -export const localTest = test.extend({ - store, - state -}) - -export const resetSelector = ( - selector: S -) => { - selector.clearCache() - selector.resetRecomputations() - selector.resetDependencyRecomputations() - selector.memoizedResultFunc.clearCache() -} - -export const logRecomputations = < - S extends OutputSelector ->( - selector: S -) => { - console.log( - `${selector.name} result function recalculated:`, - selector.recomputations(), - `time(s)`, - `input selectors recalculated:`, - selector.dependencyRecomputations(), - `time(s)` - ) -} diff --git a/test/tsconfig.json b/test/tsconfig.json index 27e1b8705..dc51ba69e 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -3,12 +3,12 @@ "compilerOptions": { "allowSyntheticDefaultImports": true, "esModuleInterop": true, - "module": "ESNext", - "moduleResolution": "Node", + "module": "esnext", + "moduleResolution": "node", "emitDeclarationOnly": false, "strict": true, "noEmit": true, - "target": "ESNext", + "target": "esnext", "jsx": "react", "baseUrl": ".", "rootDir": ".", @@ -18,10 +18,8 @@ "types": ["vitest/globals"], "paths": { "reselect": ["../src/index.ts"], // @remap-prod-remove-line - "@internal/*": ["../src/*"] + "@internal/*": ["src/*"] } }, - "include": [ - "**/*.ts", - ] + "include": ["**/*.ts"] } diff --git a/test/weakmapMemoize.spec.ts b/test/weakmapMemoize.spec.ts index 999d36248..369e121ad 100644 --- a/test/weakmapMemoize.spec.ts +++ b/test/weakmapMemoize.spec.ts @@ -1,7 +1,7 @@ import { createSelectorCreator, weakMapMemoize } from 'reselect' // Construct 1E6 states for perf test outside of the perf test so as to not change the execute time of the test function -const numOfStates = 1_000_000 +const numOfStates = 1000000 interface StateA { a: number } @@ -23,7 +23,7 @@ for (let i = 0; i < numOfStates; i++) { states.push({ a: 1, b: 2 }) } -describe('Basic selector behavior with weakMapMemoize', () => { +describe('Basic selector behavior with autotrack', () => { const createSelector = createSelectorCreator(weakMapMemoize) test('basic selector', () => { diff --git a/tsconfig.json b/tsconfig.json index 388fab465..d36a9f630 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,6 @@ "skipLibCheck": true, "allowJs": true, "jsx": "react", - "noErrorTruncation": true, "declaration": true, "emitDeclarationOnly": true, "outDir": "./es", @@ -20,7 +19,7 @@ "baseUrl": ".", "paths": { "reselect": ["src/index.ts"], // @remap-prod-remove-line - "@internal/*": ["src/*"] + "@internal/*": ["src/*"], } }, "include": ["./src/**/*"], diff --git a/type-tests/argsMemoize.test-d.ts b/type-tests/argsMemoize.test-d.ts deleted file mode 100644 index 1d58e8ee7..000000000 --- a/type-tests/argsMemoize.test-d.ts +++ /dev/null @@ -1,894 +0,0 @@ -import memoizeOne from 'memoize-one' -import microMemoize from 'micro-memoize' -import { - createSelector, - createSelectorCreator, - defaultMemoize, - unstable_autotrackMemoize as autotrackMemoize, - weakMapMemoize -} from 'reselect' -import { assertType, describe, expectTypeOf, test } from 'vitest' - -interface RootState { - todos: { - id: number - completed: boolean - }[] -} - -const state: RootState = { - todos: [ - { id: 0, completed: false }, - { id: 1, completed: false } - ] -} - -describe('memoize and argsMemoize', () => { - test('Override Only Memoize In createSelector', () => { - const selectorDefaultSeparateInlineArgs = createSelector( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { memoize: defaultMemoize } - ) - const selectorDefaultArgsAsArray = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(t => t.id), - { memoize: defaultMemoize } - ) - const selectorDefaultArgsAsArrayWithMemoizeOptions = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(t => t.id), - { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } - ) - const selectorDefaultSeparateInlineArgsWithMemoizeOptions = createSelector( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } - ) - const selectorAutotrackSeparateInlineArgs = createSelector( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { memoize: autotrackMemoize } - ) - const selectorAutotrackArgsAsArray = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(t => t.id), - { memoize: autotrackMemoize } - ) - // @ts-expect-error When memoize is autotrackMemoize, type of memoizeOptions needs to be the same as options args in autotrackMemoize. - const selectorAutotrackArgsAsArrayWithMemoizeOptions = createSelector( - [(state: RootState) => state.todos], - // @ts-expect-error - todos => todos.map(t => t.id), - { memoize: autotrackMemoize, memoizeOptions: { maxSize: 2 } } - ) - const selectorAutotrackSeparateInlineArgsWithMemoizeOptions = - // @ts-expect-error When memoize is autotrackMemoize, type of memoizeOptions needs to be the same as options args in autotrackMemoize. - createSelector( - (state: RootState) => state.todos, - // @ts-expect-error - todos => todos.map(t => t.id), - { memoize: autotrackMemoize, memoizeOptions: { maxSize: 2 } } - ) - const selectorWeakMapSeparateInlineArgs = createSelector( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { memoize: weakMapMemoize } - ) - const selectorWeakMapArgsAsArray = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(t => t.id), - { memoize: weakMapMemoize } - ) - // @ts-expect-error When memoize is weakMapMemoize, type of memoizeOptions needs to be the same as options args in weakMapMemoize. - const selectorWeakMapArgsAsArrayWithMemoizeOptions = createSelector( - [(state: RootState) => state.todos], - // @ts-expect-error - todos => todos.map(t => t.id), - { memoize: weakMapMemoize, memoizeOptions: { maxSize: 2 } } - ) - // @ts-expect-error When memoize is weakMapMemoize, type of memoizeOptions needs to be the same as options args in weakMapMemoize. - const selectorWeakMapSeparateInlineArgsWithMemoizeOptions = createSelector( - (state: RootState) => state.todos, - // @ts-expect-error - todos => todos.map(t => t.id), - { memoize: weakMapMemoize, memoizeOptions: { maxSize: 2 } } - ) - const createSelectorDefault = createSelectorCreator(defaultMemoize) - const createSelectorWeakMap = createSelectorCreator(weakMapMemoize) - const createSelectorAutotrack = createSelectorCreator(autotrackMemoize) - const changeMemoizeMethodSelectorDefault = createSelectorDefault( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { memoize: weakMapMemoize } - ) - const changeMemoizeMethodSelectorWeakMap = createSelectorWeakMap( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { memoize: defaultMemoize } - ) - const changeMemoizeMethodSelectorAutotrack = createSelectorAutotrack( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { memoize: defaultMemoize } - ) - const changeMemoizeMethodSelectorDefaultWithMemoizeOptions = - // @ts-expect-error When memoize is changed to weakMapMemoize or autotrackMemoize, memoizeOptions cannot be the same type as options args in defaultMemoize. - createSelectorDefault( - (state: RootState) => state.todos, - // @ts-expect-error - todos => todos.map(t => t.id), - { memoize: weakMapMemoize, memoizeOptions: { maxSize: 2 } } - ) - const changeMemoizeMethodSelectorWeakMapWithMemoizeOptions = - createSelectorWeakMap( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } // When memoize is changed to defaultMemoize, memoizeOptions can now be the same type as options args in defaultMemoize. - ) - const changeMemoizeMethodSelectorAutotrackWithMemoizeOptions = - createSelectorAutotrack( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } // When memoize is changed to defaultMemoize, memoizeOptions can now be the same type as options args in defaultMemoize. - ) - }) - - test('Override Only argsMemoize In createSelector', () => { - const selectorDefaultSeparateInlineArgs = createSelector( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { argsMemoize: defaultMemoize } - ) - const selectorDefaultArgsAsArray = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(t => t.id), - { argsMemoize: defaultMemoize } - ) - const selectorDefaultArgsAsArrayWithMemoizeOptions = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(t => t.id), - { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } - ) - const selectorDefaultSeparateInlineArgsWithMemoizeOptions = createSelector( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } - ) - const selectorAutotrackSeparateInlineArgs = createSelector( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { argsMemoize: autotrackMemoize } - ) - const selectorAutotrackArgsAsArray = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(t => t.id), - { argsMemoize: autotrackMemoize } - ) - // @ts-expect-error When argsMemoize is autotrackMemoize, type of argsMemoizeOptions needs to be the same as options args in autotrackMemoize. - const selectorAutotrackArgsAsArrayWithMemoizeOptions = createSelector( - [(state: RootState) => state.todos], - // @ts-expect-error - todos => todos.map(t => t.id), - { - argsMemoize: autotrackMemoize, - argsMemoizeOptions: { maxSize: 2 } - } - ) - const selectorAutotrackSeparateInlineArgsWithMemoizeOptions = - // @ts-expect-error When argsMemoize is autotrackMemoize, type of argsMemoizeOptions needs to be the same as options args in autotrackMemoize. - createSelector( - (state: RootState) => state.todos, - // @ts-expect-error - todos => todos.map(t => t.id), - { - argsMemoize: autotrackMemoize, - argsMemoizeOptions: { maxSize: 2 } - } - ) - const selectorWeakMapSeparateInlineArgs = createSelector( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { argsMemoize: weakMapMemoize } - ) - const selectorWeakMapArgsAsArray = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(t => t.id), - { argsMemoize: weakMapMemoize } - ) - // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. - const selectorWeakMapArgsAsArrayWithMemoizeOptions = createSelector( - [(state: RootState) => state.todos], - // @ts-expect-error - todos => todos.map(t => t.id), - { argsMemoize: weakMapMemoize, argsMemoizeOptions: { maxSize: 2 } } - ) - // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. - const selectorWeakMapSeparateInlineArgsWithMemoizeOptions = createSelector( - (state: RootState) => state.todos, - // @ts-expect-error - todos => todos.map(t => t.id), - { argsMemoize: weakMapMemoize, argsMemoizeOptions: { maxSize: 2 } } - ) - // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. - const selectorWeakMapSeparateInlineArgsWithMemoizeOptions1 = createSelector( - [ - (state: RootState) => state.todos, - // @ts-expect-error - todos => todos.map(t => t.id) - ], - { - argsMemoize: weakMapMemoize, - argsMemoizeOptions: { maxSize: 2 } - } - ) - // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. - const selectorWeakMapSeparateInlineArgsWithMemoizeOptions2 = createSelector( - (state: RootState) => state.todos, - // @ts-expect-error - todos => todos.map(t => t.id), - { - memoize: defaultMemoize, - argsMemoize: weakMapMemoize, - memoizeOptions: { - equalityCheck: - // @ts-expect-error - (a, b) => a === b, - maxSize: 2 - }, - argsMemoizeOptions: { maxSize: 2 } - } - ) - // const createSelectorDefaultMemoize = createSelectorCreator(defaultMemoize) - const createSelectorDefaultMemoize = createSelectorCreator({ - memoize: defaultMemoize - }) - const selectorWeakMapSeparateInlineArgsWithMemoizeOptions3 = - // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. - createSelectorDefaultMemoize( - (state: RootState) => state.todos, - // @ts-expect-error - todos => todos.map(t => t.id), - { - memoize: defaultMemoize, - argsMemoize: weakMapMemoize, - // memoizeOptions: [], - memoizeOptions: [ - { - equalityCheck: - // @ts-expect-error - (a, b) => a === b, - maxSize: 2 - } - ], - argsMemoizeOptions: [{ maxSize: 2 }] - } - ) - const selectorWeakMapSeparateInlineArgsWithMemoizeOptions4 = - // @ts-expect-error - createSelectorDefaultMemoize( - (state: RootState) => state.todos, - // @ts-expect-error - todos => todos.map(t => t.id), - { - memoizeOptions: [{ isPromise: false }], - argsMemoizeOptions: - // @ts-expect-error - (a, b) => a === b - } - ) - const selectorWeakMapSeparateInlineArgsWithMemoizeOptions5 = - // @ts-expect-error - createSelectorDefaultMemoize( - [(state: RootState) => state.todos], - // @ts-expect-error - todos => todos.map(t => t.id), - { - argsMemoize: weakMapMemoize, - memoizeOptions: [{ isPromise: false }], - argsMemoizeOptions: [] - // argsMemoizeOptions: (a, b) => a === b - } - ) - const selectorWeakMapSeparateInlineArgsWithMemoizeOptions6 = - createSelectorDefaultMemoize( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { - argsMemoize: weakMapMemoize, - memoize: weakMapMemoize, - memoizeOptions: [], - argsMemoizeOptions: [] - // argsMemoizeOptions: (a, b) => a === b - } - ) - const createSelectorDefault = createSelectorCreator(defaultMemoize) - const createSelectorWeakMap = createSelectorCreator(weakMapMemoize) - const createSelectorAutotrack = createSelectorCreator(autotrackMemoize) - const changeMemoizeMethodSelectorDefault = createSelectorDefault( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { argsMemoize: weakMapMemoize } - ) - const changeMemoizeMethodSelectorWeakMap = createSelectorWeakMap( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { argsMemoize: defaultMemoize } - ) - const changeMemoizeMethodSelectorAutotrack = createSelectorAutotrack( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { argsMemoize: defaultMemoize } - ) - const changeMemoizeMethodSelectorDefaultWithMemoizeOptions = - // @ts-expect-error When argsMemoize is changed to weakMapMemoize or autotrackMemoize, argsMemoizeOptions cannot be the same type as options args in defaultMemoize. - createSelectorDefault( - (state: RootState) => state.todos, - // @ts-expect-error - todos => todos.map(t => t.id), - { argsMemoize: weakMapMemoize, argsMemoizeOptions: { maxSize: 2 } } - ) - const changeMemoizeMethodSelectorWeakMapWithMemoizeOptions = - createSelectorWeakMap( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } // When argsMemoize is changed to defaultMemoize, argsMemoizeOptions can now be the same type as options args in defaultMemoize. - ) - const changeMemoizeMethodSelectorAutotrackWithMemoizeOptions = - createSelectorAutotrack( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } // When argsMemoize is changed to defaultMemoize, argsMemoizeOptions can now be the same type as options args in defaultMemoize. - ) - }) - - test('Override memoize And argsMemoize In createSelector', () => { - const createSelectorMicroMemoize = createSelectorCreator({ - memoize: microMemoize, - memoizeOptions: [{ isEqual: (a, b) => a === b }], - // memoizeOptions: { isEqual: (a, b) => a === b }, - argsMemoize: microMemoize, - argsMemoizeOptions: { isEqual: (a, b) => a === b } - }) - const selectorMicroMemoize = createSelectorMicroMemoize( - (state: RootState) => state.todos, - todos => todos.map(({ id }) => id) - ) - assertType(selectorMicroMemoize(state)) - // @ts-expect-error - selectorMicroMemoize() - // Checking existence of fields related to `argsMemoize` - selectorMicroMemoize.cache - selectorMicroMemoize.fn() - selectorMicroMemoize.isMemoized - selectorMicroMemoize.options - // @ts-expect-error - selectorMicroMemoize.clearCache() - // Checking existence of fields related to `memoize` - selectorMicroMemoize.memoizedResultFunc.cache - selectorMicroMemoize.memoizedResultFunc.fn() - selectorMicroMemoize.memoizedResultFunc.isMemoized - selectorMicroMemoize.memoizedResultFunc.options - // @ts-expect-error - selectorMicroMemoize.memoizedResultFunc.clearCache() - // Checking existence of fields related to the actual memoized selector - selectorMicroMemoize.dependencies - assertType< - [ - (state: RootState) => { - id: number - completed: boolean - }[] - ] - >(selectorMicroMemoize.dependencies) - assertType(selectorMicroMemoize.lastResult()) - // @ts-expect-error - selectorMicroMemoize.memoizedResultFunc() - assertType( - selectorMicroMemoize.memoizedResultFunc([{ id: 0, completed: true }]) - ) - selectorMicroMemoize.recomputations() - selectorMicroMemoize.resetRecomputations() - // @ts-expect-error - selectorMicroMemoize.resultFunc() - assertType( - selectorMicroMemoize.resultFunc([{ id: 0, completed: true }]) - ) - - // Checking to see if types dynamically change if memoize or argsMemoize are overridden inside `createSelector`. - // `microMemoize` was initially passed into `createSelectorCreator` - // as `memoize` and `argsMemoize`, After overriding them both to `defaultMemoize`, - // not only does the type for `memoizeOptions` and `argsMemoizeOptions` change to - // the options parameter of `defaultMemoize`, the output selector fields - // also change their type to the return type of `defaultMemoize`. - const selectorMicroMemoizeOverridden = createSelectorMicroMemoize( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { - memoize: defaultMemoize, - argsMemoize: defaultMemoize, - memoizeOptions: { equalityCheck: (a, b) => a === b, maxSize: 2 }, - argsMemoizeOptions: { equalityCheck: (a, b) => a === b, maxSize: 3 } - } - ) - assertType(selectorMicroMemoizeOverridden(state)) - // @ts-expect-error - selectorMicroMemoizeOverridden() - // Checking existence of fields related to `argsMemoize` - selectorMicroMemoizeOverridden.clearCache() // Prior to override, this field did NOT exist. - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverridden.cache - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverridden.fn() - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverridden.isMemoized - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverridden.options - // Checking existence of fields related to `memoize` - selectorMicroMemoizeOverridden.memoizedResultFunc.clearCache() // Prior to override, this field did NOT exist. - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverridden.memoizedResultFunc.cache - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverridden.memoizedResultFunc.fn() - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverridden.memoizedResultFunc.isMemoized - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverridden.memoizedResultFunc.options - // Checking existence of fields related to the actual memoized selector - assertType< - [ - (state: RootState) => { - id: number - completed: boolean - }[] - ] - >(selectorMicroMemoizeOverridden.dependencies) - assertType( - selectorMicroMemoizeOverridden.memoizedResultFunc([ - { id: 0, completed: true } - ]) - ) - // @ts-expect-error - selectorMicroMemoizeOverridden.memoizedResultFunc() - selectorMicroMemoizeOverridden.recomputations() - selectorMicroMemoizeOverridden.resetRecomputations() - // @ts-expect-error - selectorMicroMemoizeOverridden.resultFunc() - assertType( - selectorMicroMemoizeOverridden.resultFunc([{ id: 0, completed: true }]) - ) - // Making sure the type behavior is consistent when args are passed in as an array. - const selectorMicroMemoizeOverriddenArray = createSelectorMicroMemoize( - [(state: RootState) => state.todos], - todos => todos.map(({ id }) => id), - { - memoize: defaultMemoize, - argsMemoize: defaultMemoize, - memoizeOptions: { equalityCheck: (a, b) => a === b, maxSize: 2 }, - argsMemoizeOptions: { equalityCheck: (a, b) => a === b, maxSize: 3 } - } - ) - assertType(selectorMicroMemoizeOverriddenArray(state)) - // @ts-expect-error - selectorMicroMemoizeOverriddenArray() - // Checking existence of fields related to `argsMemoize` - selectorMicroMemoizeOverriddenArray.clearCache() // Prior to override, this field did NOT exist. - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverriddenArray.cache - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverriddenArray.fn() - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverriddenArray.isMemoized - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverriddenArray.options - // Checking existence of fields related to `memoize` - selectorMicroMemoizeOverriddenArray.memoizedResultFunc.clearCache() // Prior to override, this field did NOT exist. - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverriddenArray.memoizedResultFunc.cache - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverriddenArray.memoizedResultFunc.fn() - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverriddenArray.memoizedResultFunc.isMemoized - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverriddenArray.memoizedResultFunc.options - // Checking existence of fields related to the actual memoized selector - assertType< - [ - (state: RootState) => { - id: number - completed: boolean - }[] - ] - >(selectorMicroMemoizeOverriddenArray.dependencies) - assertType( - selectorMicroMemoizeOverriddenArray.memoizedResultFunc([ - { id: 0, completed: true } - ]) - ) - // @ts-expect-error - selectorMicroMemoizeOverriddenArray.memoizedResultFunc() - selectorMicroMemoizeOverriddenArray.recomputations() - selectorMicroMemoizeOverriddenArray.resetRecomputations() - // @ts-expect-error - selectorMicroMemoizeOverriddenArray.resultFunc() - assertType( - selectorMicroMemoizeOverriddenArray.resultFunc([ - { id: 0, completed: true } - ]) - ) - const selectorMicroMemoizeOverrideArgsMemoizeOnlyWrong = - // @ts-expect-error Because `memoizeOptions` should not contain `resultEqualityCheck`. - createSelectorMicroMemoize( - (state: RootState) => state.todos, - todos => todos.map(({ id }) => id), - { - argsMemoize: defaultMemoize, - memoizeOptions: { - isPromise: false, - resultEqualityCheck: - // @ts-expect-error - (a, b) => a === b - }, - argsMemoizeOptions: { resultEqualityCheck: (a, b) => a === b } - } - ) - const selectorMicroMemoizeOverrideArgsMemoizeOnly = - createSelectorMicroMemoize( - (state: RootState) => state.todos, - todos => todos.map(({ id }) => id), - { - argsMemoize: defaultMemoize, - memoizeOptions: { isPromise: false }, - argsMemoizeOptions: { resultEqualityCheck: (a, b) => a === b } - } - ) - assertType(selectorMicroMemoizeOverrideArgsMemoizeOnly(state)) - // @ts-expect-error - selectorMicroMemoizeOverrideArgsMemoizeOnly() - // Checking existence of fields related to `argsMemoize` - selectorMicroMemoizeOverrideArgsMemoizeOnly.clearCache() // Prior to override, this field did NOT exist. - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverrideArgsMemoizeOnly.cache - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverrideArgsMemoizeOnly.fn() - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverrideArgsMemoizeOnly.isMemoized - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverrideArgsMemoizeOnly.options - - // Checking existence of fields related to `memoize`, these should still be the same. - selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc.cache - selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc.fn() - selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc.isMemoized - selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc.options - // @ts-expect-error Note that since we did not override `memoize` in the options object, - // `memoizedResultFunc.clearCache` is still an invalid field access. - selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc.clearCache() - // Checking existence of fields related to the actual memoized selector - assertType< - [ - (state: RootState) => { - id: number - completed: boolean - }[] - ] - >(selectorMicroMemoizeOverrideArgsMemoizeOnly.dependencies) - assertType( - selectorMicroMemoizeOverrideArgsMemoizeOnly.lastResult() - ) - assertType( - selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc([ - { id: 0, completed: true } - ]) - ) - // @ts-expect-error - selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc() - selectorMicroMemoizeOverrideArgsMemoizeOnly.recomputations() - selectorMicroMemoizeOverrideArgsMemoizeOnly.resetRecomputations() - // @ts-expect-error - selectorMicroMemoizeOverrideArgsMemoizeOnly.resultFunc() - assertType( - selectorMicroMemoizeOverrideArgsMemoizeOnly.resultFunc([ - { id: 0, completed: true } - ]) - ) - - const selectorMicroMemoizeOverrideMemoizeOnly = createSelectorMicroMemoize( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { - memoize: defaultMemoize, - memoizeOptions: { resultEqualityCheck: (a, b) => a === b } - } - ) - assertType(selectorMicroMemoizeOverrideMemoizeOnly(state)) - // @ts-expect-error - selectorMicroMemoizeOverrideMemoizeOnly() - - // Checking existence of fields related to `argsMemoize` - selectorMicroMemoizeOverrideMemoizeOnly.cache - selectorMicroMemoizeOverrideMemoizeOnly.fn - selectorMicroMemoizeOverrideMemoizeOnly.isMemoized - selectorMicroMemoizeOverrideMemoizeOnly.options - // @ts-expect-error Note that since we did not override `argsMemoize` in the options object, - // `selector.clearCache` is still an invalid field access. - selectorMicroMemoizeOverrideMemoizeOnly.clearCache() - - // Checking existence of fields related to `memoize` - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc.cache - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc.fn() - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc.isMemoized - // @ts-expect-error Prior to override, this field DID exist. - selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc.options - selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc.clearCache() // Prior to override, this field did NOT exist. - - // Checking existence of fields related to the actual memoized selector - assertType< - [ - (state: RootState) => { - id: number - completed: boolean - }[] - ] - >(selectorMicroMemoizeOverrideMemoizeOnly.dependencies) - assertType(selectorMicroMemoizeOverrideMemoizeOnly.lastResult()) - assertType( - selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc([ - { id: 0, completed: true } - ]) - ) - // @ts-expect-error - selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc() - selectorMicroMemoizeOverrideMemoizeOnly.recomputations() - selectorMicroMemoizeOverrideMemoizeOnly.resetRecomputations() - // @ts-expect-error - selectorMicroMemoizeOverrideMemoizeOnly.resultFunc() - assertType( - selectorMicroMemoizeOverrideMemoizeOnly.resultFunc([ - { id: 0, completed: true } - ]) - ) - - const selectorMicroMemoizePartiallyOverridden = - // @ts-expect-error Since `argsMemoize` is set to `defaultMemoize`, `argsMemoizeOptions` must match the options object parameter of `defaultMemoize` - createSelectorMicroMemoize( - (state: RootState) => state.todos, - // @ts-expect-error - todos => todos.map(t => t.id), - { - memoize: defaultMemoize, - argsMemoize: defaultMemoize, - memoizeOptions: { - equalityCheck: - // @ts-expect-error - (a, b) => a === b, - maxSize: 2 - }, - argsMemoizeOptions: { isPromise: false } // This field causes a type error since it does not match the options param of `defaultMemoize`. - } - ) - const selectorMicroMemoizePartiallyOverridden1 = - // @ts-expect-error Since `argsMemoize` is set to `defaultMemoize`, `argsMemoizeOptions` must match the options object parameter of `defaultMemoize` - createSelectorMicroMemoize( - (state: RootState) => state.todos, - // @ts-expect-error - todos => todos.map(t => t.id), - { - memoize: defaultMemoize, - argsMemoize: defaultMemoize, - memoizeOptions: [ - { - equalityCheck: - // @ts-expect-error - (a, b) => a === b, - maxSize: 2 - } - ], - argsMemoizeOptions: [{ isPromise: false }] // This field causes a type error since it does not match the options param of `defaultMemoize`. - } - ) - const selectorMicroMemoizePartiallyOverridden2 = createSelectorMicroMemoize( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { - // memoizeOptions: [ - // { - // equalityCheck: - // // @ts-expect-error - // (a, b) => a === b, - // maxSize: 2 - // } - // ], - argsMemoizeOptions: [{ isPromise: false }] - } - ) - - const selectorDefaultParametric = createSelector( - (state: RootState, id: number) => id, - (state: RootState) => state.todos, - (id, todos) => todos.filter(todo => todo.id === id), - { - argsMemoize: microMemoize, - inputStabilityCheck: 'never', - memoize: memoizeOne, - argsMemoizeOptions: [], - memoizeOptions: [(a, b) => a === b] - } - ) - assertType< - { - id: number - completed: boolean - }[] - >(selectorDefaultParametric(state, 0)) - assertType< - { - id: number - completed: boolean - }[] - >(selectorDefaultParametric(state, 1)) - // @ts-expect-error - selectorDefaultParametric(state) - // @ts-expect-error - selectorDefaultParametric(1) - // @ts-expect-error - selectorDefaultParametric(state, '') - // @ts-expect-error - selectorDefaultParametric(state, 1, 1) - // Checking existence of fields related to `argsMemoize` - // Prior to override, this field did NOT exist. - selectorDefaultParametric.cache - // Prior to override, this field did NOT exist. - selectorDefaultParametric.fn - // Prior to override, this field did NOT exist. - selectorDefaultParametric.isMemoized - // Prior to override, this field did NOT exist. - selectorDefaultParametric.options - // @ts-expect-error Prior to override, this field DID exist. - selectorDefaultParametric.clearCache() - - // Checking existence of fields related to `memoize` - // @ts-expect-error Prior to override, this field DID exist. - selectorDefaultParametric.memoizedResultFunc.clearCache() - // Prior to override, this field did NOT exist. - selectorDefaultParametric.memoizedResultFunc.clear() - - // Checking existence of fields related to the actual memoized selector - assertType< - [ - (state: RootState, id: number) => number, - (state: RootState) => { id: number; completed: boolean }[] - ] - >(selectorDefaultParametric.dependencies) - assertType<{ id: number; completed: boolean }[]>( - selectorDefaultParametric.lastResult() - ) - assertType<{ id: number; completed: boolean }[]>( - selectorDefaultParametric.memoizedResultFunc(0, [ - { id: 0, completed: true } - ]) - ) - // @ts-expect-error - selectorDefaultParametric.memoizedResultFunc() - selectorDefaultParametric.recomputations() - selectorDefaultParametric.resetRecomputations() - // @ts-expect-error - selectorDefaultParametric.resultFunc() - assertType<{ id: number; completed: boolean }[]>( - selectorDefaultParametric.resultFunc(0, [{ id: 0, completed: true }]) - ) - }) - - test('memoize And argsMemoize In createSelectorCreator', () => { - // If we don't pass in `argsMemoize`, the type for `argsMemoizeOptions` - // falls back to the options parameter of `defaultMemoize`. - const createSelectorArgsMemoizeOptionsFallbackToDefault = - createSelectorCreator({ - memoize: microMemoize, - memoizeOptions: [{ isPromise: false }], - argsMemoizeOptions: { resultEqualityCheck: (a, b) => a === b } - }) - const selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault = - createSelectorArgsMemoizeOptionsFallbackToDefault( - (state: RootState) => state.todos, - todos => todos.map(({ id }) => id) - ) - assertType( - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault(state) - ) - // @ts-expect-error - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault() - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.resultFunc - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.clearCache() - // @ts-expect-error - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.cache - // @ts-expect-error - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.fn - // @ts-expect-error - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.isMemoized - // @ts-expect-error - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.options - // Checking existence of fields related to `memoize` - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc - .cache - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc.fn() - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc - .isMemoized - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc - .options - // @ts-expect-error - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc.clearCache() - // Checking existence of fields related to the actual memoized selector - assertType< - [ - (state: RootState) => { - id: number - completed: boolean - }[] - ] - >(selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.dependencies) - assertType( - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.lastResult() - ) - assertType( - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc( - [{ id: 0, completed: true }] - ) - ) - // @ts-expect-error - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc() - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.recomputations() - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.resetRecomputations() - // @ts-expect-error - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.resultFunc() - assertType( - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.resultFunc([ - { id: 0, completed: true } - ]) - ) - expectTypeOf( - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoize - ).toEqualTypeOf(microMemoize) - expectTypeOf( - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.argsMemoize - ).toEqualTypeOf(defaultMemoize) - - const createSelectorWithWrongArgsMemoizeOptions = - // @ts-expect-error If we don't pass in `argsMemoize`, the type for `argsMemoizeOptions` falls back to the options parameter of `defaultMemoize`. - createSelectorCreator({ - memoize: microMemoize, - memoizeOptions: { isEqual: (a, b) => a === b }, - argsMemoizeOptions: { - isEqual: - // @ts-expect-error implicit any - (a, b) => a === b - } - }) - - // When passing in an options object as the first argument, there should be no other arguments. - const createSelectorWrong = createSelectorCreator( - { - // @ts-expect-error - memoize: microMemoize, - // @ts-expect-error - memoizeOptions: { isEqual: (a, b) => a === b }, - // @ts-expect-error - argsMemoizeOptions: { equalityCheck: (a, b) => a === b } - }, - [] // This causes the error. - ) - }) - - - test('autotrackMemoize types', () => { - const selector = createSelector( - [(state: RootState) => state.todos], - todos => todos.map(t => t.id), - { memoize: autotrackMemoize } - ) - selector.memoizedResultFunc.clearCache - }) -}) diff --git a/type-tests/createSelectorCreator.test-d.ts b/type-tests/createSelectorCreator.test-d.ts deleted file mode 100644 index da5897277..000000000 --- a/type-tests/createSelectorCreator.test-d.ts +++ /dev/null @@ -1,58 +0,0 @@ -import lodashMemoize from 'lodash/memoize' -import memoizeOne from 'memoize-one' -import microMemoize from 'micro-memoize' -import { - createSelectorCreator, - defaultMemoize, - unstable_autotrackMemoize as autotrackMemoize, - weakMapMemoize -} from 'reselect' -import { describe, test } from 'vitest' - -interface RootState { - todos: { id: number; completed: boolean }[] - alerts: { id: number; read: boolean }[] -} - -const state: RootState = { - todos: [ - { id: 0, completed: false }, - { id: 1, completed: true } - ], - alerts: [ - { id: 0, read: false }, - { id: 1, read: true } - ] -} - -describe('createSelectorCreator', () => { - test('options object as argument', () => { - const createSelectorDefault = createSelectorCreator({ - memoize: defaultMemoize - }) - const createSelectorWeakMap = createSelectorCreator({ - memoize: weakMapMemoize - }) - const createSelectorAutotrack = createSelectorCreator({ - memoize: autotrackMemoize - }) - const createSelectorMicro = createSelectorCreator({ - memoize: microMemoize - }) - const createSelectorOne = createSelectorCreator({ - memoize: memoizeOne - }) - const createSelectorLodash = createSelectorCreator({ - memoize: lodashMemoize - }) - }) - - test('memoize function as argument', () => { - const createSelectorDefault = createSelectorCreator(defaultMemoize) - const createSelectorWeakMap = createSelectorCreator(weakMapMemoize) - const createSelectorAutotrack = createSelectorCreator(autotrackMemoize) - const createSelectorMicro = createSelectorCreator(microMemoize) - const createSelectorOne = createSelectorCreator(memoizeOne) - const createSelectorLodash = createSelectorCreator(lodashMemoize) - }) -}) diff --git a/type-tests/createStructuredSelector.test-d.ts b/type-tests/createStructuredSelector.test-d.ts deleted file mode 100644 index 13f16fbfe..000000000 --- a/type-tests/createStructuredSelector.test-d.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { TypedStructuredSelectorCreator } from 'reselect' -import { createStructuredSelector } from 'reselect' -import { describe, test } from 'vitest' - -interface RootState { - todos: { id: number; completed: boolean }[] - alerts: { id: number; read: boolean }[] -} - -const state: RootState = { - todos: [ - { id: 0, completed: false }, - { id: 1, completed: true } - ], - alerts: [ - { id: 0, read: false }, - { id: 1, read: true } - ] -} - -describe('createStructuredSelector', () => { - test('TypedStructuredSelectorCreator', () => { - const typedStructuredSelectorCreator: TypedStructuredSelectorCreator = - createStructuredSelector - const structuredSelector = typedStructuredSelectorCreator({ - todos: state => state.todos, - alerts: state => state.alerts - }) - structuredSelector(state).alerts - structuredSelector(state).todos - }) - test('parametric', () => { - const structuredSelector = createStructuredSelector({ - todos: (state: RootState) => state.todos, - alerts: (state: RootState) => state.alerts, - todoById: (state: RootState, id: number) => state.todos[id] - }) - structuredSelector(state, 0).alerts - structuredSelector(state, 0).todoById.id - structuredSelector(state, 0).todos - const { alerts, todos, todoById } = structuredSelector(state, 0) - }) -}) diff --git a/type-tests/deepNesting.test-d.ts b/type-tests/deepNesting.test-d.ts deleted file mode 100644 index 5a5e69bc2..000000000 --- a/type-tests/deepNesting.test-d.ts +++ /dev/null @@ -1,320 +0,0 @@ -import microMemoize from 'micro-memoize' -import { createSelector, defaultMemoize } from 'reselect' -import { describe, test } from 'vitest' - -interface RootState { - todos: { - id: number - completed: boolean - }[] -} - -const state: RootState = { - todos: [ - { id: 0, completed: false }, - { id: 1, completed: false } - ] -} - -describe('deep nesting', () => { - test('Deep Nesting First And Second createSelector Overload', () => { - type State = { foo: string } - const readOne = (state: State) => state.foo - - const selector0 = createSelector(readOne, one => one) - const selector1 = createSelector(selector0, s => s) - const selector2 = createSelector(selector1, s => s) - const selector3 = createSelector(selector2, s => s) - const selector4 = createSelector(selector3, s => s) - const selector5 = createSelector(selector4, s => s) - const selector6 = createSelector(selector5, s => s) - const selector7 = createSelector(selector6, s => s) - const selector8 = createSelector(selector7, s => s) - const selector9 = createSelector(selector8, s => s) - const selector10 = createSelector(selector9, s => s, { - memoize: microMemoize - }) - selector10.dependencies[0].dependencies[0].dependencies[0].dependencies[0] - .dependencies[0].dependencies[0].dependencies[0].dependencies[0] - .dependencies[0].dependencies[0].memoizedResultFunc.clearCache - const selector11 = createSelector(selector10, s => s) - const selector12 = createSelector(selector11, s => s) - const selector13 = createSelector(selector12, s => s) - const selector14 = createSelector(selector13, s => s) - const selector15 = createSelector(selector14, s => s) - const selector16 = createSelector(selector15, s => s) - const selector17 = createSelector(selector16, s => s) - const selector18 = createSelector(selector17, s => s) - const selector19 = createSelector(selector18, s => s) - const selector20 = createSelector(selector19, s => s) - selector20.dependencies[0].dependencies[0].dependencies[0].dependencies[0] - .dependencies[0].dependencies[0].dependencies[0].dependencies[0] - .dependencies[0].dependencies[0].memoizedResultFunc.cache - const selector21 = createSelector(selector20, s => s) - const selector22 = createSelector(selector21, s => s) - const selector23 = createSelector(selector22, s => s) - const selector24 = createSelector(selector23, s => s) - const selector25 = createSelector(selector24, s => s) - const selector26 = createSelector(selector25, s => s) - const selector27 = createSelector(selector26, s => s) - const selector28 = createSelector(selector27, s => s) - const selector29 = createSelector(selector28, s => s) - const selector30 = createSelector(selector29, s => s) - selector30.dependencies[0].dependencies[0].dependencies[0].dependencies[0] - .dependencies[0].dependencies[0].dependencies[0].dependencies[0] - .dependencies[0].dependencies[0].dependencies[0].dependencies[0] - .dependencies[0].dependencies[0].dependencies[0].dependencies[0] - .dependencies[0].dependencies[0].dependencies[0].dependencies[0] - .dependencies[0].dependencies[0].dependencies[0].dependencies[0] - .dependencies[0].dependencies[0].dependencies[0].dependencies[0] - .dependencies[0].dependencies[0].memoizedResultFunc.clearCache - }) - test('Deep Nesting Second createSelector Overload', () => { - type State = { foo: string } - const readOne = (state: State) => state.foo - - const selector0 = createSelector(readOne, one => one) - const selector1 = createSelector(selector0, s => s, { - memoize: defaultMemoize - }) - const selector2 = createSelector(selector1, s => s, { - memoize: defaultMemoize - }) - const selector3 = createSelector(selector2, s => s, { - memoize: defaultMemoize - }) - const selector4 = createSelector(selector3, s => s, { - memoize: defaultMemoize - }) - const selector5 = createSelector(selector4, s => s, { - memoize: defaultMemoize - }) - const selector6 = createSelector(selector5, s => s, { - memoize: defaultMemoize - }) - const selector7 = createSelector(selector6, s => s, { - memoize: defaultMemoize - }) - const selector8 = createSelector(selector7, s => s, { - memoize: defaultMemoize - }) - const selector9 = createSelector(selector8, s => s, { - memoize: defaultMemoize - }) - const selector10 = createSelector(selector9, s => s, { - memoize: defaultMemoize - }) - const selector11 = createSelector(selector10, s => s, { - memoize: defaultMemoize - }) - const selector12 = createSelector(selector11, s => s, { - memoize: defaultMemoize - }) - const selector13 = createSelector(selector12, s => s, { - memoize: defaultMemoize - }) - const selector14 = createSelector(selector13, s => s, { - memoize: defaultMemoize - }) - const selector15 = createSelector(selector14, s => s, { - memoize: defaultMemoize - }) - const selector16 = createSelector(selector15, s => s, { - memoize: defaultMemoize - }) - const selector17 = createSelector(selector16, s => s, { - memoize: defaultMemoize - }) - const selector18 = createSelector(selector17, s => s, { - memoize: defaultMemoize - }) - const selector19 = createSelector(selector18, s => s, { - memoize: defaultMemoize - }) - const selector20 = createSelector(selector19, s => s, { - memoize: defaultMemoize - }) - const selector21 = createSelector(selector20, s => s, { - memoize: defaultMemoize - }) - const selector22 = createSelector(selector21, s => s, { - memoize: defaultMemoize - }) - const selector23 = createSelector(selector22, s => s, { - memoize: defaultMemoize - }) - const selector24 = createSelector(selector23, s => s, { - memoize: defaultMemoize - }) - const selector25 = createSelector(selector24, s => s, { - memoize: defaultMemoize - }) - const selector26 = createSelector(selector25, s => s, { - memoize: defaultMemoize - }) - const selector27 = createSelector(selector26, s => s, { - memoize: defaultMemoize - }) - const selector28 = createSelector(selector27, s => s, { - memoize: defaultMemoize - }) - const selector29 = createSelector(selector28, s => s, { - memoize: defaultMemoize - }) - }) - - test('Deep Nesting Third createSelector Overload', () => { - type State = { foo: string } - const readOne = (state: State) => state.foo - - const selector0 = createSelector(readOne, one => one) - const selector1 = createSelector([selector0], s => s) - const selector2 = createSelector([selector1], s => s) - const selector3 = createSelector([selector2], s => s) - const selector4 = createSelector([selector3], s => s) - const selector5 = createSelector([selector4], s => s) - const selector6 = createSelector([selector5], s => s) - const selector7 = createSelector([selector6], s => s) - const selector8 = createSelector([selector7], s => s) - const selector9 = createSelector([selector8], s => s) - const selector10 = createSelector([selector9], s => s) - const selector11 = createSelector([selector10], s => s) - const selector12 = createSelector([selector11], s => s) - const selector13 = createSelector([selector12], s => s) - const selector14 = createSelector([selector13], s => s) - const selector15 = createSelector([selector14], s => s) - const selector16 = createSelector([selector15], s => s) - const selector17 = createSelector([selector16], s => s) - const selector18 = createSelector([selector17], s => s) - const selector19 = createSelector([selector18], s => s) - const selector20 = createSelector([selector19], s => s) - const selector21 = createSelector([selector20], s => s) - const selector22 = createSelector([selector21], s => s) - const selector23 = createSelector([selector22], s => s) - const selector24 = createSelector([selector23], s => s) - const selector25 = createSelector([selector24], s => s) - const selector26 = createSelector([selector25], s => s) - const selector27 = createSelector([selector26], s => s) - const selector28 = createSelector([selector27], s => s) - const selector29 = createSelector([selector28], s => s) - const selector30 = createSelector([selector29], s => s) - }) - - test('createSelector Parameter Limit', () => { - const selector = createSelector( - (state: { testString: string }) => state.testString, - (state: { testNumber: number }) => state.testNumber, - (state: { testBoolean: boolean }) => state.testBoolean, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testNumber: number }) => state.testNumber, - (state: { testStringArray: string[] }) => state.testStringArray, - (state: { testString: string }) => state.testString, - (state: { testNumber: number }) => state.testNumber, - (state: { testBoolean: boolean }) => state.testBoolean, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testNumber: number }) => state.testNumber, - (state: { testStringArray: string[] }) => state.testStringArray, - (state: { testString: string }) => state.testString, - (state: { testNumber: number }) => state.testNumber, - (state: { testBoolean: boolean }) => state.testBoolean, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testNumber: number }) => state.testNumber, - (state: { testStringArray: string[] }) => state.testStringArray, - (state: { testString: string }) => state.testString, - (state: { testNumber: number }) => state.testNumber, - (state: { testBoolean: boolean }) => state.testBoolean, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testNumber: number }) => state.testNumber, - (state: { testStringArray: string[] }) => state.testStringArray, - ( - foo1: string, - foo2: number, - foo3: boolean, - foo4: string, - foo5: string, - foo6: string, - foo7: string, - foo8: number, - foo9: string[], - foo10: string, - foo11: number, - foo12: boolean, - foo13: string, - foo14: string, - foo15: string, - foo16: string, - foo17: number, - foo18: string[], - foo19: string, - foo20: number, - foo21: boolean, - foo22: string, - foo23: string, - foo24: string, - foo25: string, - foo26: number, - foo27: string[], - foo28: string, - foo29: number, - foo30: boolean, - foo31: string, - foo32: string, - foo33: string, - foo34: string, - foo35: number, - foo36: string[] - ) => { - return { - foo1, - foo2, - foo3, - foo4, - foo5, - foo6, - foo7, - foo8, - foo9, - foo10, - foo11, - foo12, - foo13, - foo14, - foo15, - foo16, - foo17, - foo18, - foo19, - foo20, - foo21, - foo22, - foo23, - foo24, - foo25, - foo26, - foo27, - foo28, - foo29, - foo30, - foo31, - foo32, - foo33, - foo34, - foo35, - foo36 - } - } - ) - }) -}) diff --git a/type-tests/tsconfig.json b/type-tests/tsconfig.json deleted file mode 100644 index 85f60db12..000000000 --- a/type-tests/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "esModuleInterop": true, - "strict": true, - "target": "ES2015", - "declaration": true, - "noEmit": true, - "skipLibCheck": true, - "paths": { - "reselect": ["../src/index"], // @remap-prod-remove-line - "@internal/*": ["../src/*"] - } - } -} diff --git a/vitest.config.ts b/vitest.config.ts index dad9aad5b..0ee99ef30 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,7 +2,6 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { - typecheck: { tsconfig: './type-tests/tsconfig.json' }, globals: true, include: ['./test/**/*.(spec|test).[jt]s?(x)'], alias: { From 97b73eba1ee016dbc881e669dd395b722c478c17 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sat, 18 Nov 2023 22:56:25 -0600 Subject: [PATCH 45/50] Update lockfile --- yarn.lock | 185 +++--------------------------------------------------- 1 file changed, 8 insertions(+), 177 deletions(-) diff --git a/yarn.lock b/yarn.lock index 10df972ff..c6aea1739 100644 --- a/yarn.lock +++ b/yarn.lock @@ -850,26 +850,6 @@ __metadata: languageName: node linkType: hard -"@typescript/analyze-trace@npm:^0.10.1": - version: 0.10.1 - resolution: "@typescript/analyze-trace@npm:0.10.1" - dependencies: - chalk: ^4.1.2 - exit: ^0.1.2 - jsonparse: ^1.3.1 - jsonstream-next: ^3.0.0 - p-limit: ^3.1.0 - split2: ^3.2.2 - treeify: ^1.1.0 - yargs: ^16.2.0 - bin: - analyze-trace: bin/analyze-trace - print-trace-types: bin/print-trace-types - simplify-trace-types: bin/simplify-trace-types - checksum: 967cad7eeedfd4c9e1a89f94a1613b81711237567a5f0061b242691a20dbe4d67b8dd5c50b4ec884165a324029b681529ff08edbcb8bf96b68e4cc5df52f1ce1 - languageName: node - linkType: hard - "@ungap/structured-clone@npm:^1.2.0": version: 1.2.0 resolution: "@ungap/structured-clone@npm:1.2.0" @@ -1272,7 +1252,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4.0.0, chalk@npm:^4.1.2": +"chalk@npm:^4.0.0": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -1324,17 +1304,6 @@ __metadata: languageName: node linkType: hard -"cliui@npm:^7.0.2": - version: 7.0.4 - resolution: "cliui@npm:7.0.4" - dependencies: - string-width: ^4.2.0 - strip-ansi: ^6.0.0 - wrap-ansi: ^7.0.0 - checksum: ce2e8f578a4813806788ac399b9e866297740eecd4ad1823c27fd344d78b22c5f8597d548adbcc46f0573e43e21e751f39446c5a5e804a12aace402b7a315d7f - languageName: node - linkType: hard - "color-convert@npm:^2.0.1": version: 2.0.1 resolution: "color-convert@npm:2.0.1" @@ -1765,13 +1734,6 @@ __metadata: languageName: node linkType: hard -"escalade@npm:^3.1.1": - version: 3.1.1 - resolution: "escalade@npm:3.1.1" - checksum: a3e2a99f07acb74b3ad4989c48ca0c3140f69f923e56d0cba0526240ee470b91010f9d39001f2a4a313841d237ede70a729e92125191ba5d21e74b106800b133 - languageName: node - linkType: hard - "escape-string-regexp@npm:^4.0.0": version: 4.0.0 resolution: "escape-string-regexp@npm:4.0.0" @@ -1974,13 +1936,6 @@ __metadata: languageName: node linkType: hard -"exit@npm:^0.1.2": - version: 0.1.2 - resolution: "exit@npm:0.1.2" - checksum: abc407f07a875c3961e4781dfcb743b58d6c93de9ab263f4f8c9d23bb6da5f9b7764fc773f86b43dd88030444d5ab8abcb611cb680fba8ca075362b77114bba3 - languageName: node - linkType: hard - "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" @@ -2173,13 +2128,6 @@ __metadata: languageName: node linkType: hard -"get-caller-file@npm:^2.0.5": - version: 2.0.5 - resolution: "get-caller-file@npm:2.0.5" - checksum: b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9 - languageName: node - linkType: hard - "get-func-name@npm:^2.0.1, get-func-name@npm:^2.0.2": version: 2.0.2 resolution: "get-func-name@npm:2.0.2" @@ -2487,7 +2435,7 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2, inherits@npm:^2.0.3": +"inherits@npm:2": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1 @@ -2863,25 +2811,6 @@ __metadata: languageName: node linkType: hard -"jsonparse@npm:^1.2.0, jsonparse@npm:^1.3.1": - version: 1.3.1 - resolution: "jsonparse@npm:1.3.1" - checksum: 6514a7be4674ebf407afca0eda3ba284b69b07f9958a8d3113ef1005f7ec610860c312be067e450c569aab8b89635e332cee3696789c750692bb60daba627f4d - languageName: node - linkType: hard - -"jsonstream-next@npm:^3.0.0": - version: 3.0.0 - resolution: "jsonstream-next@npm:3.0.0" - dependencies: - jsonparse: ^1.2.0 - through2: ^4.0.2 - bin: - jsonstream-next: bin.js - checksum: 651d9d304ae9b23e397f0c1c60d8679daab41f42981eca6eaa61527a2f249d1cc0e8f3b3da2ce686590933ee92301d2092cb4cc65a24206d5a4e4409b77bdc21 - languageName: node - linkType: hard - "jsx-ast-utils@npm:^2.4.1 || ^3.0.0": version: 3.3.5 resolution: "jsx-ast-utils@npm:3.3.5" @@ -2999,11 +2928,9 @@ __metadata: linkType: hard "lru-cache@npm:^10.0.1, lru-cache@npm:^9.1.1 || ^10.0.0": - version: 10.0.2 - resolution: "lru-cache@npm:10.0.2" - dependencies: - semver: ^7.3.5 - checksum: 83ad0e899d79f48574bdda131fe8157c6d65cbd073a6e78e0d1a3467a85dce1ef4d8dc9fd618a56c57a068271501c81d54471e13f84dd121e046b155ed061ed4 + version: 10.0.3 + resolution: "lru-cache@npm:10.0.3" + checksum: e4b100c5a6b2ac778c0f63711499b5098686205c57907d8c04a413270d37089112d9bd0192dfa36940eb5d94b88c7db54fdb6fd23319c8f89903cfd4323ea06c languageName: node linkType: hard @@ -3408,7 +3335,7 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^3.0.2, p-limit@npm:^3.1.0": +"p-limit@npm:^3.0.2": version: 3.1.0 resolution: "p-limit@npm:3.1.0" dependencies: @@ -3709,17 +3636,6 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:3, readable-stream@npm:^3.0.0": - version: 3.6.2 - resolution: "readable-stream@npm:3.6.2" - dependencies: - inherits: ^2.0.3 - string_decoder: ^1.1.1 - util-deprecate: ^1.0.1 - checksum: bdcbe6c22e846b6af075e32cf8f4751c2576238c5043169a1c221c92ee2878458a816a4ea33f4c67623c0b6827c8a400409bfb3cf0bf3381392d0b1dfb52ac8d - languageName: node - linkType: hard - "readdirp@npm:~3.6.0": version: 3.6.0 resolution: "readdirp@npm:3.6.0" @@ -3795,13 +3711,6 @@ __metadata: languageName: node linkType: hard -"require-directory@npm:^2.1.1": - version: 2.1.1 - resolution: "require-directory@npm:2.1.1" - checksum: fb47e70bf0001fdeabdc0429d431863e9475e7e43ea5f94ad86503d918423c1543361cc5166d713eaa7029dd7a3d34775af04764bebff99ef413111a5af18c80 - languageName: node - linkType: hard - "requireindex@npm:~1.1.0": version: 1.1.0 resolution: "requireindex@npm:1.1.0" @@ -3826,7 +3735,6 @@ __metadata: "@typescript-eslint/eslint-plugin": 5.1.0 "@typescript-eslint/eslint-plugin-tslint": 5.1.0 "@typescript-eslint/parser": 5.1.0 - "@typescript/analyze-trace": ^0.10.1 eslint: ^8.0.1 eslint-plugin-react: ^7.26.1 eslint-plugin-typescript: 0.14.0 @@ -4021,13 +3929,6 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:~5.2.0": - version: 5.2.1 - resolution: "safe-buffer@npm:5.2.1" - checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 - languageName: node - linkType: hard - "safe-regex-test@npm:^1.0.0": version: 1.0.0 resolution: "safe-regex-test@npm:1.0.0" @@ -4210,15 +4111,6 @@ __metadata: languageName: node linkType: hard -"split2@npm:^3.2.2": - version: 3.2.2 - resolution: "split2@npm:3.2.2" - dependencies: - readable-stream: ^3.0.0 - checksum: 8127ddbedd0faf31f232c0e9192fede469913aa8982aa380752e0463b2e31c2359ef6962eb2d24c125bac59eeec76873678d723b1c7ff696216a1cd071e3994a - languageName: node - linkType: hard - "ssri@npm:^10.0.0": version: 10.0.5 resolution: "ssri@npm:10.0.5" @@ -4242,7 +4134,7 @@ __metadata: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -4314,15 +4206,6 @@ __metadata: languageName: node linkType: hard -"string_decoder@npm:^1.1.1": - version: 1.3.0 - resolution: "string_decoder@npm:1.3.0" - dependencies: - safe-buffer: ~5.2.0 - checksum: 8417646695a66e73aefc4420eb3b84cc9ffd89572861fe004e6aeb13c7bc00e2f616247505d2dbbef24247c372f70268f594af7126f43548565c68c117bdeb56 - languageName: node - linkType: hard - "strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" @@ -4437,15 +4320,6 @@ __metadata: languageName: node linkType: hard -"through2@npm:^4.0.2": - version: 4.0.2 - resolution: "through2@npm:4.0.2" - dependencies: - readable-stream: 3 - checksum: ac7430bd54ccb7920fd094b1c7ff3e1ad6edd94202e5528331253e5fde0cc56ceaa690e8df9895de2e073148c52dfbe6c4db74cacae812477a35660090960cc0 - languageName: node - linkType: hard - "tinybench@npm:^2.5.0": version: 2.5.1 resolution: "tinybench@npm:2.5.1" @@ -4494,13 +4368,6 @@ __metadata: languageName: node linkType: hard -"treeify@npm:^1.1.0": - version: 1.1.0 - resolution: "treeify@npm:1.1.0" - checksum: aa00dded220c1dd052573bd6fc2c52862f09870851a284f0d3650d72bf913ba9b4f6b824f4f1ab81899bae29375f4266b07fe47cbf82343a1efa13cc09ce87af - languageName: node - linkType: hard - "ts-interface-checker@npm:^0.1.9": version: 0.1.13 resolution: "ts-interface-checker@npm:0.1.13" @@ -4714,13 +4581,6 @@ __metadata: languageName: node linkType: hard -"util-deprecate@npm:^1.0.1": - version: 1.0.2 - resolution: "util-deprecate@npm:1.0.2" - checksum: 474acf1146cb2701fe3b074892217553dfcf9a031280919ba1b8d651a068c9b15d863b7303cb15bd00a862b498e6cf4ad7b4a08fb134edd5a6f7641681cb54a2 - languageName: node - linkType: hard - "vite-node@npm:0.34.6": version: 0.34.6 resolution: "vite-node@npm:0.34.6" @@ -4947,7 +4807,7 @@ __metadata: languageName: node linkType: hard -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" dependencies: @@ -4976,13 +4836,6 @@ __metadata: languageName: node linkType: hard -"y18n@npm:^5.0.5": - version: 5.0.8 - resolution: "y18n@npm:5.0.8" - checksum: 54f0fb95621ee60898a38c572c515659e51cc9d9f787fb109cef6fde4befbe1c4602dc999d30110feee37456ad0f1660fa2edcfde6a9a740f86a290999550d30 - languageName: node - linkType: hard - "yallist@npm:^4.0.0": version: 4.0.0 resolution: "yallist@npm:4.0.0" @@ -4997,28 +4850,6 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:^20.2.2": - version: 20.2.9 - resolution: "yargs-parser@npm:20.2.9" - checksum: 8bb69015f2b0ff9e17b2c8e6bfe224ab463dd00ca211eece72a4cd8a906224d2703fb8a326d36fdd0e68701e201b2a60ed7cf81ce0fd9b3799f9fe7745977ae3 - languageName: node - linkType: hard - -"yargs@npm:^16.2.0": - version: 16.2.0 - resolution: "yargs@npm:16.2.0" - dependencies: - cliui: ^7.0.2 - escalade: ^3.1.1 - get-caller-file: ^2.0.5 - require-directory: ^2.1.1 - string-width: ^4.2.0 - y18n: ^5.0.5 - yargs-parser: ^20.2.2 - checksum: b14afbb51e3251a204d81937c86a7e9d4bdbf9a2bcee38226c900d00f522969ab675703bee2a6f99f8e20103f608382936034e64d921b74df82b63c07c5e8f59 - languageName: node - linkType: hard - "yocto-queue@npm:^0.1.0": version: 0.1.0 resolution: "yocto-queue@npm:0.1.0" From ecd40bc67f84adae09fcb8496f4dce410fc79561 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sun, 19 Nov 2023 01:33:36 -0600 Subject: [PATCH 46/50] Remove other changes not related to docs --- test/reselect.bench.ts | 217 ++++++++++++ typescript_test/argsMemoize.typetest.ts | 426 +++++------------------- typescript_test/test.ts | 32 +- typescript_test/typesTestUtils.ts | 23 -- yarn.lock | 184 +++++----- 5 files changed, 413 insertions(+), 469 deletions(-) delete mode 100644 typescript_test/typesTestUtils.ts diff --git a/test/reselect.bench.ts b/test/reselect.bench.ts index e69de29bb..d212c821d 100644 --- a/test/reselect.bench.ts +++ b/test/reselect.bench.ts @@ -0,0 +1,217 @@ +import { createSelector } from '@reduxjs/toolkit' +import { bench } from 'vitest' +import { autotrackMemoize } from '../src/autotrackMemoize/autotrackMemoize' +import { weakMapMemoize } from '../src/weakMapMemoize' + +const options: NonNullable[2]> = { + iterations: 1_000_000, + time: 100 +} + +describe('bench', () => { + interface State { + todos: { + id: number + completed: boolean + }[] + } + const state: State = { + todos: [ + { id: 0, completed: false }, + { id: 1, completed: false }, + { id: 2, completed: false }, + { id: 3, completed: false }, + { id: 4, completed: false }, + { id: 5, completed: false }, + { id: 6, completed: false }, + { id: 7, completed: false }, + { id: 8, completed: false }, + { id: 9, completed: false }, + { id: 10, completed: false }, + { id: 11, completed: false }, + { id: 12, completed: false }, + { id: 13, completed: false }, + { id: 14, completed: false }, + { id: 15, completed: false }, + { id: 16, completed: false }, + { id: 17, completed: false }, + { id: 18, completed: false }, + { id: 19, completed: false }, + { id: 20, completed: false }, + { id: 21, completed: false }, + { id: 22, completed: false }, + { id: 23, completed: false }, + { id: 24, completed: false }, + { id: 25, completed: false }, + { id: 26, completed: false }, + { id: 27, completed: false }, + { id: 28, completed: false }, + { id: 29, completed: false }, + { id: 30, completed: false }, + { id: 31, completed: false }, + { id: 32, completed: false }, + { id: 33, completed: false }, + { id: 34, completed: false }, + { id: 35, completed: false }, + { id: 36, completed: false }, + { id: 37, completed: false }, + { id: 38, completed: false }, + { id: 39, completed: false }, + { id: 40, completed: false }, + { id: 41, completed: false }, + { id: 42, completed: false }, + { id: 43, completed: false }, + { id: 44, completed: false }, + { id: 45, completed: false }, + { id: 46, completed: false }, + { id: 47, completed: false }, + { id: 48, completed: false }, + { id: 49, completed: false }, + { id: 50, completed: false }, + { id: 51, completed: false }, + { id: 52, completed: false }, + { id: 53, completed: false }, + { id: 54, completed: false }, + { id: 55, completed: false }, + { id: 56, completed: false }, + { id: 57, completed: false }, + { id: 58, completed: false }, + { id: 59, completed: false }, + { id: 60, completed: false }, + { id: 61, completed: false }, + { id: 62, completed: false }, + { id: 63, completed: false }, + { id: 64, completed: false }, + { id: 65, completed: false }, + { id: 66, completed: false }, + { id: 67, completed: false }, + { id: 68, completed: false }, + { id: 69, completed: false }, + { id: 70, completed: false }, + { id: 71, completed: false }, + { id: 72, completed: false }, + { id: 73, completed: false }, + { id: 74, completed: false }, + { id: 75, completed: false }, + { id: 76, completed: false }, + { id: 77, completed: false }, + { id: 78, completed: false }, + { id: 79, completed: false }, + { id: 80, completed: false }, + { id: 81, completed: false }, + { id: 82, completed: false }, + { id: 83, completed: false }, + { id: 84, completed: false }, + { id: 85, completed: false }, + { id: 86, completed: false }, + { id: 87, completed: false }, + { id: 88, completed: false }, + { id: 89, completed: false }, + { id: 90, completed: false }, + { id: 91, completed: false }, + { id: 92, completed: false }, + { id: 93, completed: false }, + { id: 94, completed: false }, + { id: 95, completed: false }, + { id: 96, completed: false }, + { id: 97, completed: false }, + { id: 98, completed: false }, + { id: 99, completed: false } + ] + } + const selectorDefault = createSelector( + (state: State) => state.todos, + todos => todos.map(t => t.id) + ) + const selectorAutotrack = createSelector( + (state: State) => state.todos, + todos => todos.map(t => t.id), + { memoize: autotrackMemoize } + ) + const selectorWeakMap = createSelector( + (state: State) => state.todos, + todos => todos.map(t => t.id), + { memoize: weakMapMemoize } + ) + const selectorArgsAutotrack = createSelector( + (state: State) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: autotrackMemoize } + ) + const nonMemoizedSelector = (state: State) => state.todos.map(t => t.id) + const selectorArgsWeakMap = createSelector( + (state: State) => state.todos, + todos => todos.map(t => t.id), + { argsMemoize: weakMapMemoize } + ) + const parametricSelector = createSelector( + (state: State) => state.todos, + (state: State, id: number) => id, + (todos, id) => todos[id] + ) + const parametricSelectorWeakMapArgs = createSelector( + (state: State) => state.todos, + (state: State, id: number) => id, + (todos, id) => todos[id], + { + argsMemoize: weakMapMemoize + } + ) + bench( + 'selectorDefault', + () => { + selectorDefault(state) + }, + options + ) + + bench( + 'selectorAutotrack', + () => { + selectorAutotrack(state) + }, + options + ) + bench( + 'selectorWeakMap', + () => { + selectorWeakMap(state) + }, + options + ) + bench( + 'selectorArgsAutotrack', + () => { + selectorArgsAutotrack(state) + }, + options + ) + bench( + 'selectorArgsWeakMap', + () => { + selectorArgsWeakMap(state) + }, + options + ) + bench( + 'non-memoized selector', + () => { + nonMemoizedSelector(state) + }, + options + ) + bench( + 'parametricSelector', + () => { + parametricSelector(state, 0) + }, + options + ) + bench( + 'parametricSelectorWeakMapArgs', + () => { + parametricSelectorWeakMapArgs(state, 0) + }, + options + ) +}) diff --git a/typescript_test/argsMemoize.typetest.ts b/typescript_test/argsMemoize.typetest.ts index 2093b8920..acb9b1df0 100644 --- a/typescript_test/argsMemoize.typetest.ts +++ b/typescript_test/argsMemoize.typetest.ts @@ -7,15 +7,15 @@ import { unstable_autotrackMemoize as autotrackMemoize, weakMapMemoize } from 'reselect' -import { expectExactType } from './typesTestUtils' +import { expectExactType } from './test' -interface RootState { +interface State { todos: { id: number completed: boolean }[] } -const state: RootState = { +const state: State = { todos: [ { id: 0, completed: false }, { id: 1, completed: false } @@ -24,69 +24,69 @@ const state: RootState = { function overrideOnlyMemoizeInCreateSelector() { const selectorDefaultSeparateInlineArgs = createSelector( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize } ) const selectorDefaultArgsAsArray = createSelector( - [(state: RootState) => state.todos], + [(state: State) => state.todos], todos => todos.map(t => t.id), { memoize: defaultMemoize } ) const selectorDefaultArgsAsArrayWithMemoizeOptions = createSelector( - [(state: RootState) => state.todos], + [(state: State) => state.todos], todos => todos.map(t => t.id), { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } ) const selectorDefaultSeparateInlineArgsWithMemoizeOptions = createSelector( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } ) const selectorAutotrackSeparateInlineArgs = createSelector( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(t => t.id), { memoize: autotrackMemoize } ) const selectorAutotrackArgsAsArray = createSelector( - [(state: RootState) => state.todos], + [(state: State) => state.todos], todos => todos.map(t => t.id), { memoize: autotrackMemoize } ) // @ts-expect-error When memoize is autotrackMemoize, type of memoizeOptions needs to be the same as options args in autotrackMemoize. const selectorAutotrackArgsAsArrayWithMemoizeOptions = createSelector( - [(state: RootState) => state.todos], + [(state: State) => state.todos], // @ts-expect-error todos => todos.map(t => t.id), { memoize: autotrackMemoize, memoizeOptions: { maxSize: 2 } } ) // @ts-expect-error When memoize is autotrackMemoize, type of memoizeOptions needs to be the same as options args in autotrackMemoize. const selectorAutotrackSeparateInlineArgsWithMemoizeOptions = createSelector( - (state: RootState) => state.todos, + (state: State) => state.todos, // @ts-expect-error todos => todos.map(t => t.id), { memoize: autotrackMemoize, memoizeOptions: { maxSize: 2 } } ) const selectorWeakMapSeparateInlineArgs = createSelector( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(t => t.id), { memoize: weakMapMemoize } ) const selectorWeakMapArgsAsArray = createSelector( - [(state: RootState) => state.todos], + [(state: State) => state.todos], todos => todos.map(t => t.id), { memoize: weakMapMemoize } ) // @ts-expect-error When memoize is weakMapMemoize, type of memoizeOptions needs to be the same as options args in weakMapMemoize. const selectorWeakMapArgsAsArrayWithMemoizeOptions = createSelector( - [(state: RootState) => state.todos], + [(state: State) => state.todos], // @ts-expect-error todos => todos.map(t => t.id), { memoize: weakMapMemoize, memoizeOptions: { maxSize: 2 } } ) // @ts-expect-error When memoize is weakMapMemoize, type of memoizeOptions needs to be the same as options args in weakMapMemoize. const selectorWeakMapSeparateInlineArgsWithMemoizeOptions = createSelector( - (state: RootState) => state.todos, + (state: State) => state.todos, // @ts-expect-error todos => todos.map(t => t.id), { memoize: weakMapMemoize, memoizeOptions: { maxSize: 2 } } @@ -95,37 +95,37 @@ function overrideOnlyMemoizeInCreateSelector() { const createSelectorWeakMap = createSelectorCreator(weakMapMemoize) const createSelectorAutotrack = createSelectorCreator(autotrackMemoize) const changeMemoizeMethodSelectorDefault = createSelectorDefault( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(t => t.id), { memoize: weakMapMemoize } ) const changeMemoizeMethodSelectorWeakMap = createSelectorWeakMap( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize } ) const changeMemoizeMethodSelectorAutotrack = createSelectorAutotrack( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize } ) const changeMemoizeMethodSelectorDefaultWithMemoizeOptions = // @ts-expect-error When memoize is changed to weakMapMemoize or autotrackMemoize, memoizeOptions cannot be the same type as options args in defaultMemoize. createSelectorDefault( - (state: RootState) => state.todos, + (state: State) => state.todos, // @ts-expect-error todos => todos.map(t => t.id), { memoize: weakMapMemoize, memoizeOptions: { maxSize: 2 } } ) const changeMemoizeMethodSelectorWeakMapWithMemoizeOptions = createSelectorWeakMap( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } // When memoize is changed to defaultMemoize, memoizeOptions can now be the same type as options args in defaultMemoize. ) const changeMemoizeMethodSelectorAutotrackWithMemoizeOptions = createSelectorAutotrack( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize, memoizeOptions: { maxSize: 2 } } // When memoize is changed to defaultMemoize, memoizeOptions can now be the same type as options args in defaultMemoize. ) @@ -133,38 +133,38 @@ function overrideOnlyMemoizeInCreateSelector() { function overrideOnlyArgsMemoizeInCreateSelector() { const selectorDefaultSeparateInlineArgs = createSelector( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(t => t.id), { argsMemoize: defaultMemoize } ) const selectorDefaultArgsAsArray = createSelector( - [(state: RootState) => state.todos], + [(state: State) => state.todos], todos => todos.map(t => t.id), { argsMemoize: defaultMemoize } ) const selectorDefaultArgsAsArrayWithMemoizeOptions = createSelector( - [(state: RootState) => state.todos], + [(state: State) => state.todos], todos => todos.map(t => t.id), { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } ) const selectorDefaultSeparateInlineArgsWithMemoizeOptions = createSelector( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(t => t.id), { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } ) const selectorAutotrackSeparateInlineArgs = createSelector( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(t => t.id), { argsMemoize: autotrackMemoize } ) const selectorAutotrackArgsAsArray = createSelector( - [(state: RootState) => state.todos], + [(state: State) => state.todos], todos => todos.map(t => t.id), { argsMemoize: autotrackMemoize } ) // @ts-expect-error When argsMemoize is autotrackMemoize, type of argsMemoizeOptions needs to be the same as options args in autotrackMemoize. const selectorAutotrackArgsAsArrayWithMemoizeOptions = createSelector( - [(state: RootState) => state.todos], + [(state: State) => state.todos], // @ts-expect-error todos => todos.map(t => t.id), { @@ -174,7 +174,7 @@ function overrideOnlyArgsMemoizeInCreateSelector() { ) // @ts-expect-error When argsMemoize is autotrackMemoize, type of argsMemoizeOptions needs to be the same as options args in autotrackMemoize. const selectorAutotrackSeparateInlineArgsWithMemoizeOptions = createSelector( - (state: RootState) => state.todos, + (state: State) => state.todos, // @ts-expect-error todos => todos.map(t => t.id), { @@ -183,156 +183,64 @@ function overrideOnlyArgsMemoizeInCreateSelector() { } ) const selectorWeakMapSeparateInlineArgs = createSelector( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(t => t.id), { argsMemoize: weakMapMemoize } ) const selectorWeakMapArgsAsArray = createSelector( - [(state: RootState) => state.todos], + [(state: State) => state.todos], todos => todos.map(t => t.id), { argsMemoize: weakMapMemoize } ) // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. const selectorWeakMapArgsAsArrayWithMemoizeOptions = createSelector( - [(state: RootState) => state.todos], + [(state: State) => state.todos], // @ts-expect-error todos => todos.map(t => t.id), { argsMemoize: weakMapMemoize, argsMemoizeOptions: { maxSize: 2 } } ) // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. const selectorWeakMapSeparateInlineArgsWithMemoizeOptions = createSelector( - (state: RootState) => state.todos, + (state: State) => state.todos, // @ts-expect-error todos => todos.map(t => t.id), { argsMemoize: weakMapMemoize, argsMemoizeOptions: { maxSize: 2 } } ) - // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. - const selectorWeakMapSeparateInlineArgsWithMemoizeOptions1 = createSelector( - [ - (state: RootState) => state.todos, - // @ts-expect-error - todos => todos.map(t => t.id) - ], - { - argsMemoize: weakMapMemoize, - argsMemoizeOptions: { maxSize: 2 } - } - ) - // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. - const selectorWeakMapSeparateInlineArgsWithMemoizeOptions2 = createSelector( - (state: RootState) => state.todos, - // @ts-expect-error - todos => todos.map(t => t.id), - { - memoize: defaultMemoize, - argsMemoize: weakMapMemoize, - memoizeOptions: { - equalityCheck: - // @ts-expect-error - (a, b) => a === b, - maxSize: 2 - }, - argsMemoizeOptions: { maxSize: 2 } - } - ) - // const createSelectorDefaultMemoize = createSelectorCreator(defaultMemoize) - const createSelectorDefaultMemoize = createSelectorCreator({ - memoize: defaultMemoize - }) - const selectorWeakMapSeparateInlineArgsWithMemoizeOptions3 = - // @ts-expect-error When argsMemoize is weakMapMemoize, type of argsMemoizeOptions needs to be the same as options args in weakMapMemoize. - createSelectorDefaultMemoize( - (state: RootState) => state.todos, - // @ts-expect-error - todos => todos.map(t => t.id), - { - memoize: defaultMemoize, - argsMemoize: weakMapMemoize, - // memoizeOptions: [], - memoizeOptions: [ - { - equalityCheck: - // @ts-expect-error - (a, b) => a === b, - maxSize: 2 - } - ], - argsMemoizeOptions: [{ maxSize: 2 }] - } - ) - const selectorWeakMapSeparateInlineArgsWithMemoizeOptions4 = - // @ts-expect-error - createSelectorDefaultMemoize( - (state: RootState) => state.todos, - // @ts-expect-error - todos => todos.map(t => t.id), - { - memoizeOptions: [{ isPromise: false }], - argsMemoizeOptions: - // @ts-expect-error - (a, b) => a === b - } - ) - const selectorWeakMapSeparateInlineArgsWithMemoizeOptions5 = - // @ts-expect-error - createSelectorDefaultMemoize( - [(state: RootState) => state.todos], - // @ts-expect-error - todos => todos.map(t => t.id), - { - argsMemoize: weakMapMemoize, - memoizeOptions: [{ isPromise: false }], - argsMemoizeOptions: [] - // argsMemoizeOptions: (a, b) => a === b - } - ) - const selectorWeakMapSeparateInlineArgsWithMemoizeOptions6 = - createSelectorDefaultMemoize( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { - argsMemoize: weakMapMemoize, - memoize: weakMapMemoize, - memoizeOptions: [], - argsMemoizeOptions: [] - // argsMemoizeOptions: (a, b) => a === b - } - ) const createSelectorDefault = createSelectorCreator(defaultMemoize) const createSelectorWeakMap = createSelectorCreator(weakMapMemoize) const createSelectorAutotrack = createSelectorCreator(autotrackMemoize) const changeMemoizeMethodSelectorDefault = createSelectorDefault( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(t => t.id), { argsMemoize: weakMapMemoize } ) const changeMemoizeMethodSelectorWeakMap = createSelectorWeakMap( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(t => t.id), { argsMemoize: defaultMemoize } ) const changeMemoizeMethodSelectorAutotrack = createSelectorAutotrack( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(t => t.id), { argsMemoize: defaultMemoize } ) const changeMemoizeMethodSelectorDefaultWithMemoizeOptions = // @ts-expect-error When argsMemoize is changed to weakMapMemoize or autotrackMemoize, argsMemoizeOptions cannot be the same type as options args in defaultMemoize. createSelectorDefault( - (state: RootState) => state.todos, + (state: State) => state.todos, // @ts-expect-error todos => todos.map(t => t.id), { argsMemoize: weakMapMemoize, argsMemoizeOptions: { maxSize: 2 } } ) const changeMemoizeMethodSelectorWeakMapWithMemoizeOptions = createSelectorWeakMap( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(t => t.id), { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } // When argsMemoize is changed to defaultMemoize, argsMemoizeOptions can now be the same type as options args in defaultMemoize. ) const changeMemoizeMethodSelectorAutotrackWithMemoizeOptions = createSelectorAutotrack( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(t => t.id), { argsMemoize: defaultMemoize, argsMemoizeOptions: { maxSize: 2 } } // When argsMemoize is changed to defaultMemoize, argsMemoizeOptions can now be the same type as options args in defaultMemoize. ) @@ -341,13 +249,12 @@ function overrideOnlyArgsMemoizeInCreateSelector() { function overrideMemoizeAndArgsMemoizeInCreateSelector() { const createSelectorMicroMemoize = createSelectorCreator({ memoize: microMemoize, - memoizeOptions: [{ isEqual: (a, b) => a === b }], - // memoizeOptions: { isEqual: (a, b) => a === b }, + memoizeOptions: { isEqual: (a, b) => a === b }, argsMemoize: microMemoize, argsMemoizeOptions: { isEqual: (a, b) => a === b } }) const selectorMicroMemoize = createSelectorMicroMemoize( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(({ id }) => id) ) expectExactType(selectorMicroMemoize(state)) @@ -371,7 +278,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { selectorMicroMemoize.dependencies expectExactType< [ - (state: RootState) => { + (state: State) => { id: number completed: boolean }[] @@ -398,7 +305,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { // the options parameter of `defaultMemoize`, the output selector fields // also change their type to the return type of `defaultMemoize`. const selectorMicroMemoizeOverridden = createSelectorMicroMemoize( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize, @@ -433,7 +340,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { // Checking existence of fields related to the actual memoized selector expectExactType< [ - (state: RootState) => { + (state: State) => { id: number completed: boolean }[] @@ -455,7 +362,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { ) // Making sure the type behavior is consistent when args are passed in as an array. const selectorMicroMemoizeOverriddenArray = createSelectorMicroMemoize( - [(state: RootState) => state.todos], + [(state: State) => state.todos], todos => todos.map(({ id }) => id), { memoize: defaultMemoize, @@ -490,7 +397,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { // Checking existence of fields related to the actual memoized selector expectExactType< [ - (state: RootState) => { + (state: State) => { id: number completed: boolean }[] @@ -513,22 +420,20 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { const selectorMicroMemoizeOverrideArgsMemoizeOnlyWrong = // @ts-expect-error Because `memoizeOptions` should not contain `resultEqualityCheck`. createSelectorMicroMemoize( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(({ id }) => id), { argsMemoize: defaultMemoize, memoizeOptions: { isPromise: false, - resultEqualityCheck: - // @ts-expect-error - (a, b) => a === b + resultEqualityCheck: (a: unknown, b: unknown) => a === b }, argsMemoizeOptions: { resultEqualityCheck: (a, b) => a === b } } ) const selectorMicroMemoizeOverrideArgsMemoizeOnly = createSelectorMicroMemoize( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(({ id }) => id), { argsMemoize: defaultMemoize, @@ -561,7 +466,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { // Checking existence of fields related to the actual memoized selector expectExactType< [ - (state: RootState) => { + (state: State) => { id: number completed: boolean }[] @@ -588,7 +493,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { ) const selectorMicroMemoizeOverrideMemoizeOnly = createSelectorMicroMemoize( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(t => t.id), { memoize: defaultMemoize, @@ -622,7 +527,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { // Checking existence of fields related to the actual memoized selector expectExactType< [ - (state: RootState) => { + (state: State) => { id: number completed: boolean }[] @@ -648,70 +553,32 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { ]) ) - const selectorMicroMemoizePartiallyOverridden = - // @ts-expect-error Since `argsMemoize` is set to `defaultMemoize`, `argsMemoizeOptions` must match the options object parameter of `defaultMemoize` - createSelectorMicroMemoize( - (state: RootState) => state.todos, - // @ts-expect-error - todos => todos.map(t => t.id), - { - memoize: defaultMemoize, - argsMemoize: defaultMemoize, - memoizeOptions: { - equalityCheck: - // @ts-expect-error - (a, b) => a === b, - maxSize: 2 - }, - argsMemoizeOptions: { isPromise: false } // This field causes a type error since it does not match the options param of `defaultMemoize`. - } - ) - const selectorMicroMemoizePartiallyOverridden1 = - // @ts-expect-error Since `argsMemoize` is set to `defaultMemoize`, `argsMemoizeOptions` must match the options object parameter of `defaultMemoize` - createSelectorMicroMemoize( - (state: RootState) => state.todos, - // @ts-expect-error - todos => todos.map(t => t.id), - { - memoize: defaultMemoize, - argsMemoize: defaultMemoize, - memoizeOptions: [ - { - equalityCheck: - // @ts-expect-error - (a, b) => a === b, - maxSize: 2 - } - ], - argsMemoizeOptions: [{ isPromise: false }] // This field causes a type error since it does not match the options param of `defaultMemoize`. - } - ) - const selectorMicroMemoizePartiallyOverridden2 = - createSelectorMicroMemoize( - (state: RootState) => state.todos, - todos => todos.map(t => t.id), - { - // memoizeOptions: [ - // { - // equalityCheck: - // // @ts-expect-error - // (a, b) => a === b, - // maxSize: 2 - // } - // ], - argsMemoizeOptions: [{ isPromise: false }] - } - ) + // @ts-expect-error Since `argsMemoize` is set to `defaultMemoize`, + // `argsMemoizeOptions` must match the options object parameter of `defaultMemoize` + const selectorMicroMemoizePartiallyOverridden = createSelectorMicroMemoize( + (state: State) => state.todos, + // @ts-expect-error + todos => todos.map(t => t.id), + { + memoize: defaultMemoize, + argsMemoize: defaultMemoize, + memoizeOptions: { + // @ts-expect-error + equalityCheck: (a, b) => a === b, + maxSize: 2 + }, + argsMemoizeOptions: { isPromise: false } // This field causes a type error since it does not match the options param of `defaultMemoize`. + } + ) const selectorDefaultParametric = createSelector( - (state: RootState, id: number) => id, - (state: RootState) => state.todos, + (state: State, id: number) => id, + (state: State) => state.todos, (id, todos) => todos.filter(todo => todo.id === id), { argsMemoize: microMemoize, inputStabilityCheck: 'never', memoize: memoizeOne, - argsMemoizeOptions: [], memoizeOptions: [(a, b) => a === b] } ) @@ -756,8 +623,8 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { // Checking existence of fields related to the actual memoized selector expectExactType< [ - (state: RootState, id: number) => number, - (state: RootState) => { id: number; completed: boolean }[] + (state: State, id: number) => number, + (state: State) => { id: number; completed: boolean }[] ] >(selectorDefaultParametric.dependencies) expectExactType<{ id: number; completed: boolean }[]>( @@ -790,7 +657,7 @@ function memoizeAndArgsMemoizeInCreateSelectorCreator() { }) const selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault = createSelectorArgsMemoizeOptionsFallbackToDefault( - (state: RootState) => state.todos, + (state: State) => state.todos, todos => todos.map(({ id }) => id) ) expectExactType( @@ -821,7 +688,7 @@ function memoizeAndArgsMemoizeInCreateSelectorCreator() { // Checking existence of fields related to the actual memoized selector expectExactType< [ - (state: RootState) => { + (state: State) => { id: number completed: boolean }[] @@ -858,11 +725,8 @@ function memoizeAndArgsMemoizeInCreateSelectorCreator() { createSelectorCreator({ memoize: microMemoize, memoizeOptions: { isEqual: (a, b) => a === b }, - argsMemoizeOptions: { - isEqual: - // @ts-expect-error implicit any - (a, b) => a === b - } + // @ts-expect-error + argsMemoizeOptions: { isEqual: (a, b) => a === b } }) // When passing in an options object as the first argument, there should be no other arguments. @@ -912,24 +776,6 @@ function deepNesting() { selector20.dependencies[0].dependencies[0].dependencies[0].dependencies[0] .dependencies[0].dependencies[0].dependencies[0].dependencies[0] .dependencies[0].dependencies[0].memoizedResultFunc.cache - const selector21 = createSelector(selector20, s => s) - const selector22 = createSelector(selector21, s => s) - const selector23 = createSelector(selector22, s => s) - const selector24 = createSelector(selector23, s => s) - const selector25 = createSelector(selector24, s => s) - const selector26 = createSelector(selector25, s => s) - const selector27 = createSelector(selector26, s => s) - const selector28 = createSelector(selector27, s => s) - const selector29 = createSelector(selector28, s => s) - const selector30 = createSelector(selector29, s => s) - selector30.dependencies[0].dependencies[0].dependencies[0].dependencies[0] - .dependencies[0].dependencies[0].dependencies[0].dependencies[0] - .dependencies[0].dependencies[0].dependencies[0].dependencies[0] - .dependencies[0].dependencies[0].dependencies[0].dependencies[0] - .dependencies[0].dependencies[0].dependencies[0].dependencies[0] - .dependencies[0].dependencies[0].dependencies[0].dependencies[0] - .dependencies[0].dependencies[0].dependencies[0].dependencies[0] - .dependencies[0].dependencies[0].memoizedResultFunc.clearCache } function deepNesting1() { @@ -1062,121 +908,3 @@ function deepNesting2() { memoize: defaultMemoize }) } - -function parameterLimit() { - const selector = createSelector( - (state: { testString: string }) => state.testString, - (state: { testNumber: number }) => state.testNumber, - (state: { testBoolean: boolean }) => state.testBoolean, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testNumber: number }) => state.testNumber, - (state: { testStringArray: string[] }) => state.testStringArray, - (state: { testString: string }) => state.testString, - (state: { testNumber: number }) => state.testNumber, - (state: { testBoolean: boolean }) => state.testBoolean, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testNumber: number }) => state.testNumber, - (state: { testStringArray: string[] }) => state.testStringArray, - (state: { testString: string }) => state.testString, - (state: { testNumber: number }) => state.testNumber, - (state: { testBoolean: boolean }) => state.testBoolean, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testNumber: number }) => state.testNumber, - (state: { testStringArray: string[] }) => state.testStringArray, - (state: { testString: string }) => state.testString, - (state: { testNumber: number }) => state.testNumber, - (state: { testBoolean: boolean }) => state.testBoolean, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testString: string }) => state.testString, - (state: { testNumber: number }) => state.testNumber, - (state: { testStringArray: string[] }) => state.testStringArray, - ( - foo1: string, - foo2: number, - foo3: boolean, - foo4: string, - foo5: string, - foo6: string, - foo7: string, - foo8: number, - foo9: string[], - foo10: string, - foo11: number, - foo12: boolean, - foo13: string, - foo14: string, - foo15: string, - foo16: string, - foo17: number, - foo18: string[], - foo19: string, - foo20: number, - foo21: boolean, - foo22: string, - foo23: string, - foo24: string, - foo25: string, - foo26: number, - foo27: string[], - foo28: string, - foo29: number, - foo30: boolean, - foo31: string, - foo32: string, - foo33: string, - foo34: string, - foo35: number, - foo36: string[] - ) => { - return { - foo1, - foo2, - foo3, - foo4, - foo5, - foo6, - foo7, - foo8, - foo9, - foo10, - foo11, - foo12, - foo13, - foo14, - foo15, - foo16, - foo17, - foo18, - foo19, - foo20, - foo21, - foo22, - foo23, - foo24, - foo25, - foo26, - foo27, - foo28, - foo29, - foo30, - foo31, - foo32, - foo33, - foo34, - foo35, - foo36 - } - } - ) -} diff --git a/typescript_test/test.ts b/typescript_test/test.ts index 7f674d320..c6be7e511 100644 --- a/typescript_test/test.ts +++ b/typescript_test/test.ts @@ -9,7 +9,7 @@ import type { TypedUseSelectorHook } from 'react-redux' import { useSelector } from 'react-redux' import type { GetStateFromSelectors, - Selector, + ParametricSelector, SelectorResultArray, TypedStructuredSelectorCreator } from 'reselect' @@ -20,7 +20,10 @@ import { defaultEqualityCheck, defaultMemoize } from 'reselect' -import { expectExactType } from './typesTestUtils' + +export function expectType(t: T): T { + return t +} type Exact = (() => T extends A ? 1 : 0) extends () => T extends B ? 1 @@ -32,6 +35,25 @@ type Exact = (() => T extends A ? 1 : 0) extends () => T extends B : never : never +export declare type IsAny = true | false extends ( + T extends never ? true : false +) + ? True + : False + +export declare type IsUnknown = unknown extends T + ? IsAny + : False + +type Equals = IsAny< + T, + never, + IsAny +> +export function expectExactType(t: T) { + return >(u: U) => {} +} + interface StateA { a: number } @@ -142,7 +164,7 @@ function testSelectorAsCombiner() { type Component

= (props: P) => any declare function connect( - selector: Selector + selector: ParametricSelector ): (component: Component

) => Component

function testConnect() { @@ -830,14 +852,14 @@ function testTypedCreateStructuredSelector() { const selectBar = (state: RootState) => state.bar const typedStructuredSelectorCreator: TypedStructuredSelectorCreator = - createStructuredSelector + createStructuredSelector as TypedStructuredSelectorCreator typedStructuredSelectorCreator({ foo: selectFoo, bar: selectBar }) - // @ts-expect-error Because `bar` is missing. + // @ts-expect-error typedStructuredSelectorCreator({ foo: selectFoo }) diff --git a/typescript_test/typesTestUtils.ts b/typescript_test/typesTestUtils.ts deleted file mode 100644 index 90d0d5b23..000000000 --- a/typescript_test/typesTestUtils.ts +++ /dev/null @@ -1,23 +0,0 @@ -export function expectType(t: T): T { - return t -} - -export declare type IsAny = true | false extends ( - T extends never ? true : false -) - ? True - : False - -export declare type IsUnknown = unknown extends T - ? IsAny - : False - -type Equals = IsAny< - T, - never, - IsAny -> - -export function expectExactType(t: T) { - return >(u: U) => {} -} diff --git a/yarn.lock b/yarn.lock index c6aea1739..2ba7fc9a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28,9 +28,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.19.5": - version: 0.19.5 - resolution: "@esbuild/android-arm64@npm:0.19.5" +"@esbuild/android-arm64@npm:0.19.6": + version: 0.19.6 + resolution: "@esbuild/android-arm64@npm:0.19.6" conditions: os=android & cpu=arm64 languageName: node linkType: hard @@ -42,9 +42,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.19.5": - version: 0.19.5 - resolution: "@esbuild/android-arm@npm:0.19.5" +"@esbuild/android-arm@npm:0.19.6": + version: 0.19.6 + resolution: "@esbuild/android-arm@npm:0.19.6" conditions: os=android & cpu=arm languageName: node linkType: hard @@ -56,9 +56,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.19.5": - version: 0.19.5 - resolution: "@esbuild/android-x64@npm:0.19.5" +"@esbuild/android-x64@npm:0.19.6": + version: 0.19.6 + resolution: "@esbuild/android-x64@npm:0.19.6" conditions: os=android & cpu=x64 languageName: node linkType: hard @@ -70,9 +70,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.19.5": - version: 0.19.5 - resolution: "@esbuild/darwin-arm64@npm:0.19.5" +"@esbuild/darwin-arm64@npm:0.19.6": + version: 0.19.6 + resolution: "@esbuild/darwin-arm64@npm:0.19.6" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -84,9 +84,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.19.5": - version: 0.19.5 - resolution: "@esbuild/darwin-x64@npm:0.19.5" +"@esbuild/darwin-x64@npm:0.19.6": + version: 0.19.6 + resolution: "@esbuild/darwin-x64@npm:0.19.6" conditions: os=darwin & cpu=x64 languageName: node linkType: hard @@ -98,9 +98,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.19.5": - version: 0.19.5 - resolution: "@esbuild/freebsd-arm64@npm:0.19.5" +"@esbuild/freebsd-arm64@npm:0.19.6": + version: 0.19.6 + resolution: "@esbuild/freebsd-arm64@npm:0.19.6" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard @@ -112,9 +112,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.19.5": - version: 0.19.5 - resolution: "@esbuild/freebsd-x64@npm:0.19.5" +"@esbuild/freebsd-x64@npm:0.19.6": + version: 0.19.6 + resolution: "@esbuild/freebsd-x64@npm:0.19.6" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard @@ -126,9 +126,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.19.5": - version: 0.19.5 - resolution: "@esbuild/linux-arm64@npm:0.19.5" +"@esbuild/linux-arm64@npm:0.19.6": + version: 0.19.6 + resolution: "@esbuild/linux-arm64@npm:0.19.6" conditions: os=linux & cpu=arm64 languageName: node linkType: hard @@ -140,9 +140,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.19.5": - version: 0.19.5 - resolution: "@esbuild/linux-arm@npm:0.19.5" +"@esbuild/linux-arm@npm:0.19.6": + version: 0.19.6 + resolution: "@esbuild/linux-arm@npm:0.19.6" conditions: os=linux & cpu=arm languageName: node linkType: hard @@ -154,9 +154,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.19.5": - version: 0.19.5 - resolution: "@esbuild/linux-ia32@npm:0.19.5" +"@esbuild/linux-ia32@npm:0.19.6": + version: 0.19.6 + resolution: "@esbuild/linux-ia32@npm:0.19.6" conditions: os=linux & cpu=ia32 languageName: node linkType: hard @@ -168,9 +168,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.19.5": - version: 0.19.5 - resolution: "@esbuild/linux-loong64@npm:0.19.5" +"@esbuild/linux-loong64@npm:0.19.6": + version: 0.19.6 + resolution: "@esbuild/linux-loong64@npm:0.19.6" conditions: os=linux & cpu=loong64 languageName: node linkType: hard @@ -182,9 +182,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.19.5": - version: 0.19.5 - resolution: "@esbuild/linux-mips64el@npm:0.19.5" +"@esbuild/linux-mips64el@npm:0.19.6": + version: 0.19.6 + resolution: "@esbuild/linux-mips64el@npm:0.19.6" conditions: os=linux & cpu=mips64el languageName: node linkType: hard @@ -196,9 +196,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.19.5": - version: 0.19.5 - resolution: "@esbuild/linux-ppc64@npm:0.19.5" +"@esbuild/linux-ppc64@npm:0.19.6": + version: 0.19.6 + resolution: "@esbuild/linux-ppc64@npm:0.19.6" conditions: os=linux & cpu=ppc64 languageName: node linkType: hard @@ -210,9 +210,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.19.5": - version: 0.19.5 - resolution: "@esbuild/linux-riscv64@npm:0.19.5" +"@esbuild/linux-riscv64@npm:0.19.6": + version: 0.19.6 + resolution: "@esbuild/linux-riscv64@npm:0.19.6" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard @@ -224,9 +224,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.19.5": - version: 0.19.5 - resolution: "@esbuild/linux-s390x@npm:0.19.5" +"@esbuild/linux-s390x@npm:0.19.6": + version: 0.19.6 + resolution: "@esbuild/linux-s390x@npm:0.19.6" conditions: os=linux & cpu=s390x languageName: node linkType: hard @@ -238,9 +238,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.19.5": - version: 0.19.5 - resolution: "@esbuild/linux-x64@npm:0.19.5" +"@esbuild/linux-x64@npm:0.19.6": + version: 0.19.6 + resolution: "@esbuild/linux-x64@npm:0.19.6" conditions: os=linux & cpu=x64 languageName: node linkType: hard @@ -252,9 +252,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.19.5": - version: 0.19.5 - resolution: "@esbuild/netbsd-x64@npm:0.19.5" +"@esbuild/netbsd-x64@npm:0.19.6": + version: 0.19.6 + resolution: "@esbuild/netbsd-x64@npm:0.19.6" conditions: os=netbsd & cpu=x64 languageName: node linkType: hard @@ -266,9 +266,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.19.5": - version: 0.19.5 - resolution: "@esbuild/openbsd-x64@npm:0.19.5" +"@esbuild/openbsd-x64@npm:0.19.6": + version: 0.19.6 + resolution: "@esbuild/openbsd-x64@npm:0.19.6" conditions: os=openbsd & cpu=x64 languageName: node linkType: hard @@ -280,9 +280,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.19.5": - version: 0.19.5 - resolution: "@esbuild/sunos-x64@npm:0.19.5" +"@esbuild/sunos-x64@npm:0.19.6": + version: 0.19.6 + resolution: "@esbuild/sunos-x64@npm:0.19.6" conditions: os=sunos & cpu=x64 languageName: node linkType: hard @@ -294,9 +294,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.19.5": - version: 0.19.5 - resolution: "@esbuild/win32-arm64@npm:0.19.5" +"@esbuild/win32-arm64@npm:0.19.6": + version: 0.19.6 + resolution: "@esbuild/win32-arm64@npm:0.19.6" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -308,9 +308,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.19.5": - version: 0.19.5 - resolution: "@esbuild/win32-ia32@npm:0.19.5" +"@esbuild/win32-ia32@npm:0.19.6": + version: 0.19.6 + resolution: "@esbuild/win32-ia32@npm:0.19.6" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard @@ -322,9 +322,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.19.5": - version: 0.19.5 - resolution: "@esbuild/win32-x64@npm:0.19.5" +"@esbuild/win32-x64@npm:0.19.6": + version: 0.19.6 + resolution: "@esbuild/win32-x64@npm:0.19.6" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -1658,31 +1658,31 @@ __metadata: linkType: hard "esbuild@npm:^0.19.3": - version: 0.19.5 - resolution: "esbuild@npm:0.19.5" - dependencies: - "@esbuild/android-arm": 0.19.5 - "@esbuild/android-arm64": 0.19.5 - "@esbuild/android-x64": 0.19.5 - "@esbuild/darwin-arm64": 0.19.5 - "@esbuild/darwin-x64": 0.19.5 - "@esbuild/freebsd-arm64": 0.19.5 - "@esbuild/freebsd-x64": 0.19.5 - "@esbuild/linux-arm": 0.19.5 - "@esbuild/linux-arm64": 0.19.5 - "@esbuild/linux-ia32": 0.19.5 - "@esbuild/linux-loong64": 0.19.5 - "@esbuild/linux-mips64el": 0.19.5 - "@esbuild/linux-ppc64": 0.19.5 - "@esbuild/linux-riscv64": 0.19.5 - "@esbuild/linux-s390x": 0.19.5 - "@esbuild/linux-x64": 0.19.5 - "@esbuild/netbsd-x64": 0.19.5 - "@esbuild/openbsd-x64": 0.19.5 - "@esbuild/sunos-x64": 0.19.5 - "@esbuild/win32-arm64": 0.19.5 - "@esbuild/win32-ia32": 0.19.5 - "@esbuild/win32-x64": 0.19.5 + version: 0.19.6 + resolution: "esbuild@npm:0.19.6" + dependencies: + "@esbuild/android-arm": 0.19.6 + "@esbuild/android-arm64": 0.19.6 + "@esbuild/android-x64": 0.19.6 + "@esbuild/darwin-arm64": 0.19.6 + "@esbuild/darwin-x64": 0.19.6 + "@esbuild/freebsd-arm64": 0.19.6 + "@esbuild/freebsd-x64": 0.19.6 + "@esbuild/linux-arm": 0.19.6 + "@esbuild/linux-arm64": 0.19.6 + "@esbuild/linux-ia32": 0.19.6 + "@esbuild/linux-loong64": 0.19.6 + "@esbuild/linux-mips64el": 0.19.6 + "@esbuild/linux-ppc64": 0.19.6 + "@esbuild/linux-riscv64": 0.19.6 + "@esbuild/linux-s390x": 0.19.6 + "@esbuild/linux-x64": 0.19.6 + "@esbuild/netbsd-x64": 0.19.6 + "@esbuild/openbsd-x64": 0.19.6 + "@esbuild/sunos-x64": 0.19.6 + "@esbuild/win32-arm64": 0.19.6 + "@esbuild/win32-ia32": 0.19.6 + "@esbuild/win32-x64": 0.19.6 dependenciesMeta: "@esbuild/android-arm": optional: true @@ -1730,7 +1730,7 @@ __metadata: optional: true bin: esbuild: bin/esbuild - checksum: 5a0227cf6ffffa3076714d88230af1dfdd2fc363d91bd712a81fb91230c315a395e2c9b7588eee62986aeebf4999804b9b1b59eeab8e2457184eb0056bfe20c8 + checksum: b5f6e19c9f3e5302ffea4ad0ba39e17f7eed09f342f04d5561cfa491a69334095655ac2a9166c29a80da14af35cfcaaaf7751f8b2bad870d49ccdb8817921f37 languageName: node linkType: hard From 42dad9af27124458ddfde64924a223133bed9230 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Mon, 20 Nov 2023 16:52:07 -0600 Subject: [PATCH 47/50] Fix links in JSDocs to match links in`README` --- src/autotrackMemoize/autotrackMemoize.ts | 2 ++ src/createSelectorCreator.ts | 18 +++++++++--------- src/createStructuredSelector.ts | 6 +++--- src/types.ts | 4 ++-- src/weakMapMemoize.ts | 2 ++ 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/autotrackMemoize/autotrackMemoize.ts b/src/autotrackMemoize/autotrackMemoize.ts index e5d698a0e..91113aa51 100644 --- a/src/autotrackMemoize/autotrackMemoize.ts +++ b/src/autotrackMemoize/autotrackMemoize.ts @@ -65,6 +65,8 @@ import { createCache } from './autotracking' * * @template Func - The type of the function that is memoized. * + * @see {@link https://github.com/reduxjs/reselect#unstable_autotrackmemoizefunc---since-500 autotrackMemoize} + * * @since 5.0.0 * @public * @experimental diff --git a/src/createSelectorCreator.ts b/src/createSelectorCreator.ts index 85525ca9c..3045472d9 100644 --- a/src/createSelectorCreator.ts +++ b/src/createSelectorCreator.ts @@ -45,7 +45,7 @@ export interface CreateSelectorFunction< * @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`. * @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`. * - * @see {@link https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-selectoroptions createSelector} + * @see {@link https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-createselectoroptions createSelector} */ ( ...createSelectorArgs: [ @@ -71,7 +71,7 @@ export interface CreateSelectorFunction< * @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`. * @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`. * - * @see {@link https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-selectoroptions createSelector} + * @see {@link https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-createselectoroptions createSelector} */ < InputSelectors extends SelectorArray, @@ -112,7 +112,7 @@ export interface CreateSelectorFunction< * @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`. * @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`. * - * @see {@link https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-selectoroptions createSelector} + * @see {@link https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-createselectoroptions createSelector} */ < InputSelectors extends SelectorArray, @@ -149,7 +149,7 @@ let globalStabilityCheck: StabilityCheckFrequency = 'once' * This function allows you to override this setting for all of your selectors. * * **Note**: This setting can still be overridden per selector inside `createSelector`'s `options` object. - * See {@link https://github.com/reduxjs/reselect#per-selector-configuration per-selector-configuration} + * See {@link https://github.com/reduxjs/reselect#2-per-selector-by-passing-an-inputstabilitycheck-option-directly-to-createselector per-selector-configuration} * and {@linkcode CreateSelectorOptions.inputStabilityCheck inputStabilityCheck} for more details. * * _The input stability check does not run in production builds._ @@ -171,8 +171,8 @@ import { OutputSelectorFields, Mapped } from './types'; * // Never run the input stability check. * setInputStabilityCheckEnabled('never') * ``` - * @see {@link https://github.com/reduxjs/reselect#development-only-checks development-only-checks} - * @see {@link https://github.com/reduxjs/reselect#global-configuration global-configuration} + * @see {@link https://github.com/reduxjs/reselect#debugging-tools debugging-tools} + * @see {@link https://github.com/reduxjs/reselect#1-globally-through-setinputstabilitycheckenabled global-configuration} * * @since 5.0.0 * @public @@ -211,7 +211,7 @@ export function setInputStabilityCheckEnabled( * @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). * @template ArgsMemoizeFunction - The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used. * - * @see {@link https://github.com/reduxjs/reselect#createselectorcreatormemoize-memoizeoptions createSelectorCreator} + * @see {@link https://github.com/reduxjs/reselect#createselectorcreatormemoize--options-memoizeoptions createSelectorCreator} * * @since 5.0.0 * @public @@ -251,7 +251,7 @@ export function createSelectorCreator< * * @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). * - * @see {@link https://github.com/reduxjs/reselect#createselectorcreatormemoize-memoizeoptions createSelectorCreator} + * @see {@link https://github.com/reduxjs/reselect#createselectorcreatormemoize--options-memoizeoptions createSelectorCreator} * * @public */ @@ -459,7 +459,7 @@ export function createSelectorCreator< * a single "result function" / "combiner", and an optional options object, and * generates a memoized selector function. * - * @see {@link https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-selectoroptions createSelector} + * @see {@link https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-createselectoroptions createSelector} * * @public */ diff --git a/src/createStructuredSelector.ts b/src/createStructuredSelector.ts index 11462b925..3ca2ae2fc 100644 --- a/src/createStructuredSelector.ts +++ b/src/createStructuredSelector.ts @@ -59,7 +59,7 @@ interface SelectorsObject { * The structured selector can take multiple input selectors * and map their output to an object with specific keys. * - * @see {@link https://github.com/reduxjs/reselect#createstructuredselectorinputselectors-selectorcreator--createselector createStructuredSelector} + * @see {@link https://github.com/reduxjs/reselect#createstructuredselector-inputselectorsobject--selectorcreator--createselector createStructuredSelector} * * @public */ @@ -171,7 +171,7 @@ export interface StructuredSelectorCreator { * @template MemoizeFunction - The type of the memoize function that is used to create the structured selector. It defaults to `defaultMemoize`. * @template ArgsMemoizeFunction - The type of the of the memoize function that is used to memoize the arguments passed into the generated structured selector. It defaults to `defaultMemoize`. * - * @see {@link https://github.com/reduxjs/reselect#createstructuredselectorinputselectors-selectorcreator--createselector createStructuredSelector} + * @see {@link https://github.com/reduxjs/reselect#createstructuredselector-inputselectorsobject--selectorcreator--createselector createStructuredSelector} */ < InputSelectorsObject extends SelectorsObject, @@ -234,7 +234,7 @@ export interface StructuredSelectorCreator { * })) * ``` * - * @see {@link https://github.com/reduxjs/reselect#createstructuredselectorinputselectors-selectorcreator--createselector createStructuredSelector} + * @see {@link https://github.com/reduxjs/reselect#createstructuredselector-inputselectorsobject--selectorcreator--createselector createStructuredSelector} * * @public */ diff --git a/src/types.ts b/src/types.ts index c79719a37..775d9c370 100644 --- a/src/types.ts +++ b/src/types.ts @@ -75,9 +75,9 @@ export interface CreateSelectorOptions< * * @default 'once' * - * @see {@link https://github.com/reduxjs/reselect#development-only-checks development-only-checks} + * @see {@link https://github.com/reduxjs/reselect#debugging-tools debugging-tools} * @see {@link https://github.com/reduxjs/reselect#inputstabilitycheck inputStabilityCheck} - * @see {@link https://github.com/reduxjs/reselect#per-selector-configuration per-selector-configuration} + * @see {@link https://github.com/reduxjs/reselect#2-per-selector-by-passing-an-inputstabilitycheck-option-directly-to-createselector per-selector-configuration} * * @since 5.0.0 */ diff --git a/src/weakMapMemoize.ts b/src/weakMapMemoize.ts index 5311c9078..f92061616 100644 --- a/src/weakMapMemoize.ts +++ b/src/weakMapMemoize.ts @@ -91,6 +91,8 @@ function createCacheNode(): CacheNode { * * @template Func - The type of the function that is memoized. * + * @see {@link https://github.com/reduxjs/reselect#weakmapmemoizefunc---since-500 weakMapMemoize} + * * @since 5.0.0 * @public * @experimental From 999ca66785519712b4d073785f6289cc42fb7e4b Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Mon, 20 Nov 2023 21:08:33 -0500 Subject: [PATCH 48/50] Remove some details wrappers and add additional descriptions --- README.md | 139 +++++++++++++++++++++++++----------------------------- 1 file changed, 65 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 775100972..7a705d8ce 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ In addition to skipping unnecessary recalculations, `memoizedSelectCompletedTodo - [Why is my selector recomputing when the input state stays the same?](#why-is-my-selector-recomputing-when-the-input-state-stays-the-same) - [Can I use Reselect without Redux?](#can-i-use-reselect-without-redux) - [How do I create a selector that takes an argument?](#how-do-i-create-a-selector-that-takes-an-argument) - - [The default memoization function is no good, can I use a different one?](#the-default-memoization-function-is-no-good-can-i-use-a-different-one) + - [Can the memoization behavior be customized?](#can-the-memoization-behavior-be-customized) - [How do I test a selector?](#how-do-i-test-a-selector) - [Can I share a selector across multiple component instances?](#can-i-share-a-selector-across-multiple-component-instances) - [Are there TypeScript Typings?](#are-there-typescript-typings) @@ -142,8 +142,6 @@ In addition to skipping unnecessary recalculations, `memoizedSelectCompletedTodo ## Terminology -

Click to expand - - [**Selector Function**](#selector-function): A function that accepts one or more JavaScript values as arguments, and derives a result. When used with [Redux], the first argument is typically the entire Redux store state. - [**input selectors**](#input-selectors): Basic selector functions used as building blocks for creating a memoized selector. They are passed as the first argument(s) to [`createSelector`], and are called with all selector arguments. They are responsible for extracting and providing necessary values to the [result function]. - [**Output Selector**](#output-selector): The actual memoized selectors created by [`createSelector`]. @@ -159,8 +157,6 @@ const outputSelector = createSelector( ) ``` -
- --- @@ -184,7 +180,9 @@ In this pattern, the `finalSelector` is composed of several [input selectors], * ### Cascading Memoization -
Click to expand +Reselect uses a two-stage "cascading" approach to memoizing functions: + +
Detailed Explanation: Cascading Memoization The way Reselect works can be broken down into multiple parts: @@ -206,11 +204,7 @@ The way Reselect works can be broken down into multiple parts: This behavior is what we call **_Cascading Double-Layer Memoization_**. -
- -### Reselect Vs Standard Memoization - -
Click to expand +#### Reselect Vs Standard Memoization ##### Standard Memoization @@ -231,8 +225,14 @@ This feature becomes crucial in [Redux] applications, where the `state` changes > [!NOTE] > The [input selectors] take the same arguments as the [output selector]. +
+ ### Why Reselect Is Often Used With [Redux] +While Reselect can be used independently from Redux, it is a standard tool used in most Redux applications to help optimize calculations and UI updates: + +
Detailed Explanation: Reselect and Redux Optimization + Imagine you have a selector like this: ```ts @@ -258,9 +258,11 @@ Now when you call `selectCompletedTodos`, it re-runs, because we have effectivel ```ts selectCompletedTodos(store.getState()) -selectCompletedTodos(store.getState()) // Will not run, and the cached result will be returned. +// Will not run, and the cached result will be returned. +selectCompletedTodos(store.getState()) store.dispatch(toggleRead(0)) -selectCompletedTodos(store.getState()) // It recalculates. +// It recalculates. +selectCompletedTodos(store.getState()) ``` But why? `selectCompletedTodos` only needs to access `state.todos`, and has nothing to do with `state.alerts`, so why have we broken memoization? Well that's because in [Redux] anytime you make a change to the root `state`, it gets shallowly updated, which means its reference changes, therefore a normal memoization function will always fail the comparison check on the arguments. @@ -278,9 +280,12 @@ And now we have achieved memoization: ```ts selectCompletedTodos(store.getState()) -selectCompletedTodos(store.getState()) // Will not run, and the cached result will be returned. +// Will not run, and the cached result will be returned. +selectCompletedTodos(store.getState()) store.dispatch(toggleRead(0)) -selectCompletedTodos(store.getState()) // The `input selectors` will run, But the `result function` is skipped and the cached result will be returned. +// The `input selectors` will run, but the `result function` is +// skipped and the cached result will be returned. +selectCompletedTodos(store.getState()) ``` Even when the overall `state` changes, Reselect ensures efficient memoization through its unique approach. The [result function] doesn't re-run if the relevant part of the `state` (in this case `state.todos`), remains unchanged. This is due to Reselect's [**_Cascading Double-Layer Memoization_**][**_Cascading Memoization_**]. The first layer checks the entire `state`, and the second layer checks the results of the [input selectors]. If the first layer fails (due to a change in the overall `state`) but the second layer succeeds (because `state.todos` is unchanged), Reselect skips recalculating the [result function]. This dual-check mechanism makes Reselect particularly effective in [Redux] applications, ensuring computations are only done when truly necessary. @@ -871,9 +876,9 @@ const selectItemsByCategory = createSelector( ) selectItemsByCategory(state, 'Electronics') // Selector runs -selectItemsByCategory(state, 'Electronics') +selectItemsByCategory(state, 'Electronics') // Cached selectItemsByCategory(state, 'Stationery') // Selector runs -selectItemsByCategory(state, 'Electronics') +selectItemsByCategory(state, 'Electronics') // Still cached! ``` This solves the problem of having to know and set the cache size prior to creating a memoized selector. Because `weakMapMemoize` essentially provides a dynamic cache size out of the box. @@ -1049,8 +1054,6 @@ const selectTodoIds = createSelectorAutotrack( -
Development-only Checks - (since 5.0.0) - #### `inputStabilityCheck` @@ -1123,13 +1126,11 @@ const selectCompletedTodosLength = createSelector( > [!WARNING] > This will override the global input stability check set by calling `setInputStabilityCheckEnabled`. -
- ### Output Selector Fields -
The output selectors created by createSelector have several additional properties attached to them: +The output selectors created by createSelector have several additional properties attached to them: | Name | Description | | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -1144,8 +1145,6 @@ const selectCompletedTodosLength = createSelector( | `memoize` | Function used to memoize the `resultFunc`. | | `argsMemoize` | Function used to memoize the arguments passed into the [output selector]. | -
- --- @@ -1154,7 +1153,7 @@ const selectCompletedTodosLength = createSelector( ## What's New in 5.0.0? -
Version 5.0.0 introduces several new features and improvements: +Version 5.0.0 introduces several new features and improvements: - **Customization Enhancements**: @@ -1365,20 +1364,16 @@ const selectCompletedTodos = createSelector( ### Why isn’t my selector recomputing when the input state changes? -
Answer - Check that your memoization function is compatible with your state update function (i.e. the reducer if you are using [Redux]). For example, a selector created with [`createSelector`] will not work with a state update function that mutates an existing object instead of creating a new one each time. [`createSelector`] uses an identity check (`===`) to detect that an input has changed, so mutating an existing object will not trigger the selector to recompute because mutating an object does not change its identity. Note that if you are using [Redux], mutating the state object is [almost certainly a mistake](http://redux.js.org/docs/Troubleshooting.html). -
- ### Why is my selector recomputing when the input state stays the same? -
Answer - To address unexpected recomputations in your selector, first ensure that `inputStabilityCheck` is set to either `'always'` or `'once'`. This setting aids in debugging by monitoring the stability of your inputs. Additionally, utilize [output selector fields] such as `recomputations`, `resetRecomputations`, `dependencyRecomputations`, and `resetDependencyRecomputations`. These tools help identify the source of the issue. Keep an eye on the `dependencyRecomputations` count. If it increases while `recomputations` remains the same, it suggests that your arguments are changing references but your [input selectors] are stable which is typically the desired behavior. +
Detailed Explanation: Selector Recomputations + To delve deeper, you can determine which arguments are changing references too frequently by using the `argsMemoizeOptions` and `equalityCheck`. Consider the following example: ```ts @@ -1411,15 +1406,22 @@ const selectAlertsByType = createSelector( ### Can I use Reselect without [Redux]? -
Answer - Yes. Reselect has no dependencies on any other package, so although it was designed to be used with [Redux] it can be used independently. It can be used with any plain JS data, such as typical [React] state values, as long as that data is being updated immutably. -
- ### How do I create a selector that takes an argument? -
Answer +Each of the [input selectors] you provide will be called with all of the selector's arguments. You can add additional input selectors to extract arguments and forward them to the [result function], like this: + +```ts +const selectTodosByCategory = createSelector( + (state: RootState) => state.todos, + // Extract the second argument to pass it on + (state: RootState, category: string) => category, + (todos, category) => todos.filter(t => t.category === category) +) +``` + +
Detailed Explanation: Selectors and Arguments When creating a selector that accepts arguments in Reselect, it's important to structure your input and [output selector]s appropriately. Here are key points to consider: @@ -1473,7 +1475,7 @@ const finalResult = items.filter(item => item.category === category && item.id !== id) ``` -
Additional Examples +In this example, `selectItemId` expects that its second argument will be some simple value, while `selectVendorName` expects that the second argument is an object. If you call `selectItemById(state, 42)`, `selectVendorName` will break because it's trying to access `42.name`. Reselect's TS types should detect this and prevent compilation: ```ts const selectItems = (state: RootState) => state.items @@ -1493,25 +1495,17 @@ const selectItemById = createSelector( ) ``` -In this example, `selectItemId` expects that its second argument will be some simple value, while `selectVendorName` expects that the second argument is an object. If you call `selectItemById(state, 42)`, `selectVendorName` will break because it's trying to access `42.name`. -
-
- -### The default memoization function is no good, can I use a different one? +### Can the memoization behavior be customized? -
Answer - -We think it works great for a lot of use cases, but sure. See [these examples](#customize-equalitycheck-for-defaultmemoize). - -
+Yes. The built-in `defaultMemoize` memoizer works great for a lot of use cases, but it can be customized or swapped out for a different memoizer. See [these examples](#customize-equalitycheck-for-defaultmemoize). ### How do I test a selector? -
Answer +Selectors are pure functions - for a given input, a selector should always produce the same result. For this reason they are simple to unit test: call the selector with a set of inputs, and assert that the result value matches an expected shape. -For a given input, a selector should always produce the same result. For this reason they are simple to unit test. +
Detailed Explanation: Testing Selectors ```ts interface RootState { @@ -1577,33 +1571,36 @@ test('selector unit test', () => { ### Can I share a selector across multiple component instances? -
Answer - -Yes, as of 5.0.0 you can use [`weakMapMemoize`](#weakmapmemoizefunc---since-500) to achieve this. +Yes, although if they pass in different arguments, you will need to handle that in order for memoization to work consistently: -
+- Pass a larger `maxSize` if using `defaultMemoize` ( as of 4.1.0+) +- Use [`weakMapMemoize`](#weakmapmemoize) (as of 5.0.0+) ### Are there TypeScript Typings? -
Answer - Yes! Reselect is now written in TypeScript itself, so they should Just Work™. -
- ### I am seeing a TypeScript error: `Type instantiation is excessively deep and possibly infinite` -
Answer - Starting in 5.0.0 you should be able to nest up to 30 selectors, but in case you still run into this issue, you can refer to [this comment](https://github.com/reduxjs/reselect/issues/534#issuecomment-956708953) for a discussion of the problem, as relating to nested selectors. -
- ### How can I make a [curried](https://github.com/hemanth/functional-programming-jargon#currying) selector? -
Answer +Selectors that take arguments are commonly used inside of React-Redux's `useSelector` by using a closure to pass along the extra arguments: + +```ts +function TodosList({ category }) { + const filteredTodos = useSelector(state => + selectTodosByCategory(state, category) + ) +} +``` + +If you prefer to use a curried form instead, you can create a curried selector with this recipe: + +
Detailed Explanation: Creating Curried Selectors You can try this pattern: @@ -1722,9 +1719,11 @@ const MyComponent: FC = ({ id }) => {
-### How can I make pre-typed version of [`createSelector`](#createselectorinputselectors--inputselectors-resultfunc-createselectoroptions) for my root state? +### How can I make pre-typed version of [`createSelector`](#createselector) for my root state? -
Answer +When used with Redux, it's typical to have all input selectors take `(state: RootState)` as their first argument. Creating a pre-typed version of `createSelector` can shorten that repetition. + +
Detailed Explanation: Pre-Typed `createSelector` You can create a custom typed version of [`createSelector`] by defining a utility type that extends the original [`createSelector`] function. Here's an example: @@ -1776,11 +1775,9 @@ export const createAppSelector: TypedCreateSelector = createSelector
-### What if I want to use [`createSelector`](#createselectorinputselectors--inputselectors-resultfunc-createselectoroptions) without memoization? - -
Answer +### What if I want to use [`createSelector`](#createselector) without memoization? -Create an [`identity function`][Identity Function]: +There may be rare cases when you might want to use `createSelector` for its composition syntax, but without any memoization applied. In that case, create an [`identity function`][Identity Function] and use it as the memoizers: ```ts const identity = any>(func: Func) => func @@ -1791,8 +1788,6 @@ const createNonMemoizedSelector = createSelectorCreator({ }) ``` -
- --- @@ -1810,8 +1805,6 @@ const createNonMemoizedSelector = createSelectorCreator({ ## Related Projects -
Click to expand - ### [re-reselect](https://github.com/toomuchdesign/re-reselect) Enhances Reselect selectors by wrapping [`createSelector`] and returning a memoized collection of selectors indexed with the cache key returned by a custom resolver function. @@ -1838,8 +1831,6 @@ Inspired by Reselect Tools, so it also has all functionality from this library a - Selectors Output (In case if selector not dependent from external arguments) - Shows "Not Memoized (NM)" selectors -
- --- From 47fd0b575352ee96652fc5e3f4459954557e1da8 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Mon, 20 Nov 2023 21:15:38 -0500 Subject: [PATCH 49/50] Tweak TOC --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7a705d8ce..2094e06d9 100644 --- a/README.md +++ b/README.md @@ -108,16 +108,17 @@ In addition to skipping unnecessary recalculations, `memoizedSelectCompletedTodo - [Terminology](#terminology) - [How Does Reselect Work?](#how-does-reselect-work) - [Cascading Memoization](#cascading-memoization) - - [Reselect Vs Standard Memoization](#reselect-vs-standard-memoization) - [Why Reselect Is Often Used With Redux](#why-reselect-is-often-used-with-redux) - [API](#api) - [**`createSelector`**][`createSelector`] + - [**`createSelectorCreator`**][`createSelectorCreator`] + - [**`createStructuredSelector`**][`createStructuredSelector`] - [**`defaultMemoize`**][`defaultMemoize`] - [**`weakMapMemoize`**][`weakMapMemoize`] - [**`unstable_autotrackMemoize`**][`unstable_autotrackMemoize`] - - [**`createSelectorCreator`**][`createSelectorCreator`] - - [**`createStructuredSelector`**][`createStructuredSelector`] - [Debugging Tools](#debuggingtools) + - [Development-Only Stability Checks](#development-only-stability-checks) + - [Output Selector Fields](#output-selector-fields) - [What's New in 5.0.0?](#v5summary) - [Optimizing Reselect](#optimizing-reselect) - [FAQ](#faq) @@ -1054,6 +1055,10 @@ const selectTodoIds = createSelectorAutotrack( +### Development-Only Stability Checks + +Reselect includes extra checks in development mode to help catch and warn about mistakes in selector behavior. + #### `inputStabilityCheck` From c536318b7200a1732155c3ae238d25a93202363a Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Mon, 20 Nov 2023 21:19:07 -0500 Subject: [PATCH 50/50] Remove "Top-Level Selectors" since createSlice handles this in RTK 2.0 --- README.md | 41 ----------------------------------------- 1 file changed, 41 deletions(-) diff --git a/README.md b/README.md index 2094e06d9..34bd845b3 100644 --- a/README.md +++ b/README.md @@ -1324,47 +1324,6 @@ const selectorBad = createSelector( --- -## Additional Tips & Tricks - -
Top Level Selectors Pattern - -This pattern simplifies the creation of selectors in your application. Start by defining a generic type for your top-level selectors: - -```ts -import type { Selector } from 'reselect' - -export type TopLevelSelectors = { - [K in keyof State as K extends string - ? `select${Capitalize}` - : never]: Selector -} -``` - -With this setup, you can easily create top-level selectors for your application's state: - -```ts -const topLevelSelectors: TopLevelSelectors = { - selectAlerts: state => state.alerts, - selectTodos: state => state.todos, - selectUsers: state => state.users -} -``` - -This approach allows for more streamlined and readable selector creation. For example: - -```ts -const selectCompletedTodos = createSelector( - [topLevelSelectors.selectTodos], - todos => todos.filter(todo => todo.completed === true) -) -``` - -
- - - ---- - ## FAQ ### Why isn’t my selector recomputing when the input state changes?