diff --git a/src/createSelectorCreator.ts b/src/createSelectorCreator.ts index 1286b18f6..8eb741cfd 100644 --- a/src/createSelectorCreator.ts +++ b/src/createSelectorCreator.ts @@ -1,4 +1,5 @@ import { defaultMemoize } from './defaultMemoize' + import type { Combiner, CreateSelectorOptions, @@ -12,12 +13,14 @@ import type { StabilityCheckFrequency, UnknownMemoizer } from './types' + import { assertIsFunction, collectInputSelectorResults, ensureIsArray, getDependencies, - runStabilityCheck + runStabilityCheck, + shouldRunInputStabilityCheck } from './utils' /** @@ -30,7 +33,17 @@ export interface CreateSelectorFunction< MemoizeFunction extends UnknownMemoizer, ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize > { - /** Input selectors as separate inline arguments */ + /** + * Creates a memoized selector function. + * + * @param createSelectorArgs - An arbitrary number of input selectors as separate inline arguments and a `combiner` function. + * @returns An 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`. + */ ( ...createSelectorArgs: [ ...inputSelectors: InputSelectors, @@ -43,7 +56,17 @@ export interface CreateSelectorFunction< ArgsMemoizeFunction > - /** Input selectors as separate inline arguments with memoizeOptions passed */ + /** + * Creates a memoized selector function. + * + * @param createSelectorArgs - An arbitrary number of input selectors as separate inline arguments, a `combiner` function and an `options` object. + * @returns An 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`. + */ < InputSelectors extends SelectorArray, Result, @@ -116,8 +139,8 @@ 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} - * and {@linkcode CreateSelectorOptions.inputStabilityCheck | inputStabilityCheck} for more details. + * See {@link https://github.com/reduxjs/reselect#per-selector-configuration per-selector-configuration} + * and {@linkcode CreateSelectorOptions.inputStabilityCheck inputStabilityCheck} for more details. * * _The input stability check does not run in production builds._ * @@ -136,8 +159,8 @@ let globalStabilityCheck: StabilityCheckFrequency = 'once' * // 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#development-only-checks development-only-checks} + * @see {@link https://github.com/reduxjs/reselect#global-configuration global-configuration} */ export function setInputStabilityCheckEnabled( inputStabilityCheckFrequency: StabilityCheckFrequency @@ -252,14 +275,14 @@ export function createSelectorCreator< : memoizeOrOptions const createSelector = < - Selectors extends SelectorArray, + InputSelectors extends SelectorArray, Result, OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction, OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction >( ...funcs: [ - ...inputSelectors: [...Selectors], - combiner: Combiner, + ...inputSelectors: [...InputSelectors], + combiner: Combiner, createSelectorOptions?: Partial< CreateSelectorOptions< MemoizeFunction, @@ -287,7 +310,7 @@ export function createSelectorCreator< // Normally, the result func or "combiner" is the last arg let resultFunc = funcs.pop() as - | Combiner + | Combiner | Partial< CreateSelectorOptions< MemoizeFunction, @@ -301,7 +324,7 @@ export function createSelectorCreator< if (typeof resultFunc === 'object') { directlyPassedOptions = resultFunc // and pop the real result func off - resultFunc = funcs.pop() as Combiner + resultFunc = funcs.pop() as Combiner } assertIsFunction( @@ -310,7 +333,7 @@ export function createSelectorCreator< ) // Determine which set of options we're using. Prefer options passed directly, - // but fall back to options given to createSelectorCreator. + // but fall back to options given to `createSelectorCreator`. const combinedOptions = { ...createSelectorCreatorOptions, ...directlyPassedOptions @@ -331,14 +354,17 @@ export function createSelectorCreator< // we wrap it in an array so we can apply it. const finalMemoizeOptions = ensureIsArray(memoizeOptions) const finalArgsMemoizeOptions = ensureIsArray(argsMemoizeOptions) - const dependencies = getDependencies(funcs) as Selectors + const dependencies = getDependencies(funcs) as InputSelectors const memoizedResultFunc = memoize(function recomputationWrapper() { recomputations++ // apply arguments instead of spreading for performance. // @ts-ignore - return (resultFunc as Combiner).apply(null, arguments) - }, ...finalMemoizeOptions) as Combiner & + return (resultFunc as Combiner).apply( + null, + arguments + ) + }, ...finalMemoizeOptions) as Combiner & ExtractMemoizerFields let firstRun = true @@ -360,12 +386,7 @@ export function createSelectorCreator< arguments ) - const shouldRunInputStabilityCheck = - process.env.NODE_ENV !== 'production' && - (inputStabilityCheck === 'always' || - (inputStabilityCheck === 'once' && firstRun)) - - if (shouldRunInputStabilityCheck) { + if (shouldRunInputStabilityCheck(inputStabilityCheck, firstRun)) { // make a second copy of the params, to check if we got the same results const inputSelectorResultsCopy = collectInputSelectorResults( dependencies, @@ -387,9 +408,9 @@ export function createSelectorCreator< return lastResult }, ...finalArgsMemoizeOptions) as Selector< - GetStateFromSelectors, + GetStateFromSelectors, Result, - GetParamsFromSelectors + GetParamsFromSelectors > & ExtractMemoizerFields @@ -403,7 +424,7 @@ export function createSelectorCreator< memoize, argsMemoize }) as OutputSelector< - Selectors, + InputSelectors, Result, OverrideMemoizeFunction, OverrideArgsMemoizeFunction diff --git a/src/types.ts b/src/types.ts index cf78afae9..d48f7bb30 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,20 +21,18 @@ export type Selector< // The result will be inferred Result = unknown, // There are either 0 params, or N params - Params extends never | readonly any[] = any[] + Params extends readonly any[] = any[] // If there are 0 params, type the function as just State in, Result out. // Otherwise, type it as State + Params in, Result out. -> = [Params] extends [never] - ? (state: State) => Result - : (state: State, ...params: Params) => Result -// /** -// * A function that takes a state and returns data that is based on that state. -// * -// * @param state - The first argument, often a Redux root state object. -// * @param params - All additional arguments passed into the selector. -// * @returns A derived value from the state. -// */ -// (state: State, ...params: FallbackIfNever) => Result +> = + /** + * A function that takes a state and returns data that is based on that state. + * + * @param state - The first argument, often a Redux root state object. + * @param params - All additional arguments passed into the selector. + * @returns A derived value from the state. + */ + (state: State, ...params: FallbackIfNever) => Result /** * A function that takes input selectors' return values as arguments and returns a result. Otherwise known as `resultFunc`. @@ -44,21 +42,23 @@ export type Selector< */ export type Combiner = /** + * A function that takes input selectors' return values as arguments and returns a result. Otherwise known as `resultFunc`. + * * @param resultFuncArgs - Return values of input selectors. - * @returns The return value of {@linkcode OutputSelectorFields.resultFunc | resultFunc}. + * @returns The return value of {@linkcode OutputSelectorFields.resultFunc resultFunc}. */ (...resultFuncArgs: SelectorResultArray) => Result /** * The additional fields attached to the output selector generated by `createSelector`. * - * **Note**: Although {@linkcode CreateSelectorOptions.memoize | memoize} - * and {@linkcode CreateSelectorOptions.argsMemoize | argsMemoize} are included in the attached fields, + * **Note**: Although {@linkcode CreateSelectorOptions.memoize memoize} + * and {@linkcode CreateSelectorOptions.argsMemoize argsMemoize} are included in the attached fields, * the fields themselves are independent of the type of - * {@linkcode CreateSelectorOptions.memoize | memoize} and {@linkcode CreateSelectorOptions.argsMemoize | argsMemoize} functions. + * {@linkcode CreateSelectorOptions.memoize memoize} and {@linkcode CreateSelectorOptions.argsMemoize argsMemoize} functions. * Meaning this type is not going to generate additional fields based on what functions we use to memoize our selectors. * - * _This type is not to be confused with {@linkcode ExtractMemoizerFields | ExtractMemoizerFields}._ + * _This type is not to be confused with {@linkcode ExtractMemoizerFields ExtractMemoizerFields}._ * * @template InputSelectors - The type of the input selectors. * @template Result - The type of the result returned by the `resultFunc`. @@ -66,8 +66,8 @@ export type Combiner = * @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. */ export interface OutputSelectorFields< - InputSelectors extends SelectorArray, - Result, + InputSelectors extends SelectorArray = SelectorArray, + Result = unknown, MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize > extends Required< @@ -78,7 +78,7 @@ export interface OutputSelectorFields< > { /** The final function passed to `createSelector`. Otherwise known as the `combiner`. */ resultFunc: Combiner - /** The memoized version of {@linkcode resultFunc | resultFunc}. */ + /** The memoized version of {@linkcode resultFunc resultFunc}. */ memoizedResultFunc: Combiner & ExtractMemoizerFields /** Returns the last result calculated by the output selector. */ @@ -100,25 +100,20 @@ export interface OutputSelectorFields< * @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. */ export type OutputSelector< - InputSelectors extends SelectorArray, - Result, + InputSelectors extends SelectorArray = SelectorArray, + Result = unknown, MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize -> = Selector< - GetStateFromSelectors, +> = PrepareOutputSelector< + InputSelectors, Result, - GetParamsFromSelectors + MemoizeFunction, + ArgsMemoizeFunction > & - ExtractMemoizerFields & - OutputSelectorFields< - InputSelectors, - Result, - MemoizeFunction, - ArgsMemoizeFunction - > + ExtractMemoizerFields /** - * A helper type designed to optimize TypeScript performance by composing parts of {@linkcode OutputSelector | OutputSelector} in a more statically structured manner. + * A helper type designed to optimize TypeScript performance by composing parts of {@linkcode OutputSelector OutputSelector} in a more statically structured manner. * * This is achieved by utilizing the `extends` keyword with `interfaces`, as opposed to creating intersections with type aliases. * This approach offers some performance benefits: @@ -127,32 +122,32 @@ export type OutputSelector< * - When checking against an intersection type, every constituent is verified before checking against the "effective" flattened type. * * This optimization focuses on resolving much of the type composition for - * {@linkcode OutputSelector | OutputSelector} using `extends` with `interfaces`, - * rather than relying on intersections for the entire {@linkcode OutputSelector | OutputSelector}. + * {@linkcode OutputSelector OutputSelector} using `extends` with `interfaces`, + * rather than relying on intersections for the entire {@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 `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/microsoft/TypeScript/wiki/Performance#preferring-interfaces-over-intersections | Reference} + * @see {@link https://github.com/microsoft/TypeScript/wiki/Performance#preferring-interfaces-over-intersections Reference} */ -// export interface PrepareOutputSelector< -// InputSelectors extends SelectorArray = SelectorArray, -// Result = unknown, -// MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, -// ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize -// > extends OutputSelectorFields< -// InputSelectors, -// Result, -// MemoizeFunction, -// ArgsMemoizeFunction -// >, -// Selector< -// GetStateFromSelectors, -// Result, -// GetParamsFromSelectors -// > {} +export interface PrepareOutputSelector< + InputSelectors extends SelectorArray = SelectorArray, + Result = unknown, + MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize, + ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize +> extends OutputSelectorFields< + InputSelectors, + Result, + MemoizeFunction, + ArgsMemoizeFunction + >, + Selector< + GetStateFromSelectors, + Result, + GetParamsFromSelectors + > {} /** * A selector that is assumed to have one additional argument, such as @@ -202,14 +197,14 @@ 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#inputstabilitycheck | inputStabilityCheck} - * @see {@link https://github.com/reduxjs/reselect#per-selector-configuration | per-selector-configuration} + * @see {@link https://github.com/reduxjs/reselect#development-only-checks development-only-checks} + * @see {@link https://github.com/reduxjs/reselect#inputstabilitycheck inputStabilityCheck} + * @see {@link https://github.com/reduxjs/reselect#per-selector-configuration per-selector-configuration} */ inputStabilityCheck?: StabilityCheckFrequency /** - * The memoize function that is used to memoize the {@linkcode OutputSelectorFields.resultFunc | resultFunc} + * The memoize function that is used to memoize the {@linkcode OutputSelectorFields.resultFunc resultFunc} * inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). * * When passed directly into `createSelector`, it overrides the `memoize` function initially passed into `createSelectorCreator`. @@ -233,8 +228,8 @@ export interface CreateSelectorOptions< > /** - * Optional configuration options for the {@linkcode memoize | memoize} function. - * These options are passed to the {@linkcode memoize | memoize} function as the second argument. + * Optional configuration options for the {@linkcode memoize memoize} function. + * These options are passed to the {@linkcode memoize memoize} function as the second argument. */ // Should dynamically change to the options argument of `memoize`. memoizeOptions?: OverrideMemoizeOptions< @@ -243,8 +238,8 @@ export interface CreateSelectorOptions< > /** - * Optional configuration options for the {@linkcode argsMemoize | argsMemoize} function. - * These options are passed to the {@linkcode argsMemoize | argsMemoize} function as the second argument. + * Optional configuration options for the {@linkcode argsMemoize argsMemoize} function. + * These options are passed to the {@linkcode argsMemoize argsMemoize} function as the second argument. */ argsMemoizeOptions?: OverrideMemoizeOptions< ArgsMemoizeFunction, @@ -323,7 +318,7 @@ export type MemoizeOptionsFromParameters< > = DropFirstParameter[0] | DropFirstParameter /** - * Extract the additional fields that a memoize function attaches to the function it memoizes (e.g., `clearCache`). + * Extracts the additional fields that a memoize function attaches to the function it memoizes (e.g., `clearCache`). * * @template MemoizeFunction - The type of the memoize function to be checked. */ @@ -360,7 +355,7 @@ export type Has = [U1] extends [U] ? 1 : 0 /** * An if-else-like type that resolves depending on whether the given type is `never`. * This is mainly used to conditionally resolve the type of a `memoizeOptions` object based on whether `memoize` is provided or not. - * @see {@link https://github.com/sindresorhus/type-fest/blob/main/source/if-never.d.ts | Source} + * @see {@link https://github.com/sindresorhus/type-fest/blob/main/source/if-never.d.ts Source} */ export type IfNever = [T] extends [never] ? TypeIfNever @@ -369,7 +364,7 @@ export type IfNever = [T] extends [never] /** * Omit any index signatures from the given object type, leaving only explicitly defined properties. * This is mainly used to remove explicit `any`s from the return type of some memoizers (e.g, `microMemoize`). - * @see {@link https://github.com/sindresorhus/type-fest/blob/main/source/omit-index-signature.d.ts | Source} + * @see {@link https://github.com/sindresorhus/type-fest/blob/main/source/omit-index-signature.d.ts Source} */ export type OmitIndexSignature = { [KeyType in keyof ObjectType as {} extends Record diff --git a/src/utils.ts b/src/utils.ts index e03927ef8..939b7e71b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,8 +2,10 @@ import type { CreateSelectorOptions, Selector, SelectorArray, + StabilityCheckFrequency, UnknownMemoizer } from './types' + /** * Assert that the provided value is a function. If the assertion fails, * a `TypeError` is thrown with an optional custom error message. @@ -129,3 +131,22 @@ export function runStabilityCheck( ) } } + +/** + * Determines if the input stability check should run. + * + * @param inputStabilityCheck - The frequency of the input stability check. + * @param firstRun - Indicates whether it is the first time the selector has run. + * @returns true if the input stability check should run, otherwise false. + */ +export const shouldRunInputStabilityCheck = ( + inputStabilityCheck: StabilityCheckFrequency, + firstRun: boolean +) => { + return ( + process.env.NODE_ENV !== 'production' && + (inputStabilityCheck === 'always' || + (inputStabilityCheck === 'once' && firstRun)) + ) +} + diff --git a/test/reselect.spec.ts b/test/reselect.spec.ts index 1eb14bf4a..658f55e57 100644 --- a/test/reselect.spec.ts +++ b/test/reselect.spec.ts @@ -1,5 +1,7 @@ // TODO: Add test for React Redux connect function +import type { PayloadAction } from '@reduxjs/toolkit' +import { configureStore, createSlice } from '@reduxjs/toolkit' import lodashMemoize from 'lodash/memoize' import microMemoize from 'micro-memoize' import { @@ -10,6 +12,11 @@ import { weakMapMemoize } from 'reselect' +import type { OutputSelector, OutputSelectorFields } from 'reselect' +// Since Node 16 does not support `structuredClone` +const deepClone = (object: T): T => + JSON.parse(JSON.stringify(object)) + // 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 interface StateA { @@ -106,7 +113,7 @@ describe('Basic selector behavior', () => { process.env.NODE_ENV = 'production' }) afterAll(() => { - process.env.NODE_NV = originalEnv + process.env.NODE_ENV = originalEnv }) test('basic selector cache hit performance', () => { @@ -390,7 +397,8 @@ describe('Customizing selectors', () => { expect(memoizer3Calls).toBeGreaterThan(0) }) - test('passing memoize directly to createSelector', () => { + + test.todo('Test order of execution in a selector', () => { interface State { todos: { id: number @@ -403,308 +411,175 @@ describe('Customizing selectors', () => { { id: 1, completed: false } ] } - const selectorDefault = createSelector( + // original options untouched. + const selectorOriginal = createSelector( (state: State) => state.todos, - todos => todos.map(t => t.id), + todos => todos.map(({ id }) => id), { - argsMemoize: defaultMemoize, - argsMemoizeOptions: { - equalityCheck: (a, b) => a === b, - resultEqualityCheck: (a, b) => a === b + 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(t => t.id === id), - { - argsMemoize: defaultMemoize, - memoize: defaultMemoize - } + (id, todos) => todos.filter(todo => todo.id === id) ) - selectorDefaultParametric(state, 0) selectorDefaultParametric(state, 1) selectorDefaultParametric(state, 1) - selectorDefaultParametric( - { - todos: [ - { id: 0, completed: false }, - { id: 1, completed: false } - ] - }, - 1 - ) - selectorDefaultParametric( - { - todos: [ - { id: 0, completed: false }, - { id: 1, completed: false } - ] - }, - 0 - ) - selectorDefaultParametric( - { - todos: [ - { id: 0, completed: false }, - { id: 1, completed: false } - ] - }, - 0 - ) - const createSelectorDefaultObj = createSelectorCreator({ - memoize: defaultMemoize - }) - const createSelectorDefaultFunc = createSelectorCreator(defaultMemoize) - const createSelectorMicroObj = createSelectorCreator({ - memoize: microMemoize - }) - const createSelectorMicroFunc = createSelectorCreator(microMemoize) - const createSelectorMicroObjWithArgsMemoize = createSelectorCreator({ - memoize: microMemoize, - argsMemoize: microMemoize, - memoizeOptions: { isEqual: (a, b) => a === b }, - argsMemoizeOptions: { isEqual: (a, b) => a === b } - }) - const createSelectorLodashFunc = createSelectorCreator(lodashMemoize) - const selectorLodashFunc = createSelectorLodashFunc( - (state: State) => state.todos, - todos => todos.map(t => t.id) - ) - const selectorDefaultObj = createSelectorDefaultObj( - (state: State) => state.todos, - todos => todos.map(t => t.id) - ) + }) +}) - // @ts-expect-error - expect(selectorDefaultObj.fn).toBeUndefined() - // @ts-expect-error - expect(selectorDefaultObj.cache).toBeUndefined() - // @ts-expect-error - expect(selectorDefaultObj.isMemoized).toBeUndefined() - // @ts-expect-error - expect(selectorDefaultObj.options).toBeUndefined() - expect(selectorDefaultObj.lastResult).toBeDefined() - expect(selectorDefaultObj.recomputations).toBeDefined() - expect(selectorDefaultObj.dependencies).toBeDefined() - expect(selectorDefaultObj.resetRecomputations).toBeDefined() - expect(selectorDefaultObj.resultFunc).toBeDefined() - expect(selectorDefaultObj.clearCache).toBeDefined() - expect(selectorDefaultObj.memoizedResultFunc).toBeDefined() - // @ts-expect-error - expect(selectorDefaultObj.memoizedResultFunc.cache).toBeUndefined() - expect(selectorDefaultObj.memoizedResultFunc.clearCache).toBeDefined() - const selectorDefaultFunc = createSelectorDefaultFunc( - (state: State) => state.todos, - todos => todos.map(t => t.id) - ) - // @ts-expect-error - expect(selectorDefaultFunc.fn).toBeUndefined() - // @ts-expect-error - expect(selectorDefaultFunc.cache).toBeUndefined() - // @ts-expect-error - expect(selectorDefaultFunc.isMemoized).toBeUndefined() - // @ts-expect-error - expect(selectorDefaultFunc.options).toBeUndefined() - expect(selectorDefaultFunc.lastResult).toBeDefined() - expect(selectorDefaultFunc.recomputations).toBeDefined() - expect(selectorDefaultFunc.dependencies).toBeDefined() - expect(selectorDefaultFunc.resetRecomputations).toBeDefined() - expect(selectorDefaultFunc.resultFunc).toBeDefined() - expect(selectorDefaultFunc.clearCache).toBeDefined() - expect(selectorDefaultFunc.memoizedResultFunc).toBeDefined() - // @ts-expect-error - expect(selectorDefaultFunc.memoizedResultFunc.cache).toBeUndefined() - expect(selectorDefaultFunc.memoizedResultFunc.clearCache).toBeDefined() - const selectorMicroFunc = createSelectorMicroFunc( - (state: State) => state.todos, - todos => todos.map(t => t.id) - ) - const selectorMicroObj = createSelectorMicroObj( - (state: State) => state.todos, - todos => todos.map(t => t.id) - ) - selectorMicroObj(state) - // @ts-expect-error - expect(selectorMicroObj.fn).toBeUndefined() - // @ts-expect-error - expect(selectorMicroObj.cache).toBeUndefined() - // @ts-expect-error - expect(selectorMicroObj.isMemoized).toBeUndefined() - // @ts-expect-error - expect(selectorMicroObj.options).toBeUndefined() - expect(selectorMicroObj.lastResult).toBeDefined() - expect(selectorMicroObj.recomputations).toBeDefined() - expect(selectorMicroObj.dependencies).toBeDefined() - expect(selectorMicroObj.resetRecomputations).toBeDefined() - expect(selectorMicroObj.resultFunc).toBeDefined() - expect(selectorMicroObj.clearCache).toBeDefined() - expect(selectorMicroObj.memoizedResultFunc).toBeDefined() - expect(selectorMicroObj.memoizedResultFunc.cache).toBeDefined() - // @ts-expect-error - expect(selectorMicroObj.memoizedResultFunc.clearCache).toBeUndefined() - // @ts-expect-error - expect(selectorMicroFunc.fn).toBeUndefined() - // @ts-expect-error - expect(selectorMicroFunc.cache).toBeUndefined() - // @ts-expect-error - expect(selectorMicroFunc.isMemoized).toBeUndefined() - // @ts-expect-error - expect(selectorMicroFunc.options).toBeUndefined() - expect(selectorMicroFunc.lastResult).toBeDefined() - expect(selectorMicroFunc.recomputations).toBeDefined() - expect(selectorMicroFunc.dependencies).toBeDefined() - expect(selectorMicroFunc.resetRecomputations).toBeDefined() - expect(selectorMicroFunc.resultFunc).toBeDefined() - expect(selectorMicroFunc.clearCache).toBeDefined() - expect(selectorMicroFunc.memoizedResultFunc).toBeDefined() - expect(selectorMicroFunc.memoizedResultFunc.cache).toBeDefined() - // @ts-expect-error - expect(selectorMicroFunc.memoizedResultFunc.clearCache).toBeUndefined() - // `memoizeOptions` should match params of `microMemoize` - const selectorMicroObj1 = createSelectorMicroObj( - (state: State) => state.todos, - todos => todos.map(t => t.id), - { memoizeOptions: { isEqual: (a, b) => a === b } } - ) - // @ts-expect-error - expect(selectorMicroObj1.fn).toBeUndefined() - // @ts-expect-error - expect(selectorMicroObj1.cache).toBeUndefined() - // @ts-expect-error - expect(selectorMicroObj1.isMemoized).toBeUndefined() - // @ts-expect-error - expect(selectorMicroObj1.options).toBeUndefined() - expect(selectorMicroObj1.lastResult).toBeDefined() - expect(selectorMicroObj1.recomputations).toBeDefined() - expect(selectorMicroObj1.dependencies).toBeDefined() - expect(selectorMicroObj1.resetRecomputations).toBeDefined() - expect(selectorMicroObj1.resultFunc).toBeDefined() - // Because `argsMemoize` is `defaultMemoize`. - expect(selectorMicroObj1.clearCache).toBeDefined() - expect(selectorMicroObj1.memoizedResultFunc).toBeDefined() - // This is undefined because `memoize` is set to `microMemoize`. - // @ts-expect-error - expect(selectorMicroObj1.memoizedResultFunc.clearCache).toBeUndefined() - expect(selectorMicroObj1.memoizedResultFunc.cache).toBeDefined() - expect(selectorMicroObj1.memoizedResultFunc.fn).toBeDefined() - expect(selectorMicroObj1.memoizedResultFunc.isMemoized).toBeDefined() - expect(selectorMicroObj1.memoizedResultFunc.options).toBeDefined() - // memoizeOptions should match params of defaultMemoize - const selectorMicroObj2 = createSelectorMicroObj( - (state: State) => state.todos, - todos => todos.map(t => t.id), - { memoize: defaultMemoize } - ) - const selectorMicroObj3 = createSelectorMicroObj( - (state: State) => state.todos, - todos => todos.map(t => t.id), - { - memoize: defaultMemoize, - memoizeOptions: { equalityCheck: (a: any, b: any) => a === b }, - argsMemoize: microMemoize, - argsMemoizeOptions: { isEqual: (a, b) => a === b } +interface TodoState { + todos: { + id: number + completed: boolean + }[] +} + +const initialState: TodoState = { + todos: [ + { id: 0, completed: false }, + { id: 1, completed: false } + ] +} + +const todoSlice = createSlice({ + name: 'todos', + initialState, + reducers: { + toggleCompleted: (state, action: PayloadAction) => { + const todo = state.todos.find(todo => todo.id === action.payload) + if (todo) { + todo.completed = !todo.completed } - ) - expect(selectorMicroObj3(state)).toBeDefined() - // @ts-expect-error - expect(selectorMicroObj2.fn).toBeUndefined() - // @ts-expect-error - expect(selectorMicroObj2.cache).toBeUndefined() - // @ts-expect-error - expect(selectorMicroObj2.isMemoized).toBeUndefined() - // @ts-expect-error - expect(selectorMicroObj2.options).toBeUndefined() - expect(selectorMicroObj2.lastResult).toBeDefined() - expect(selectorMicroObj2.recomputations).toBeDefined() - expect(selectorMicroObj2.dependencies).toBeDefined() - expect(selectorMicroObj2.resetRecomputations).toBeDefined() - expect(selectorMicroObj2.resultFunc).toBeDefined() - // Because argsMemoize is defaultMemoize - expect(selectorMicroObj2.clearCache).toBeDefined() - expect(selectorMicroObj2.memoizedResultFunc).toBeDefined() - // @ts-expect-error - expect(selectorMicroObj2.memoizedResultFunc.cache).toBeUndefined() - // Because memoize is - expect(selectorMicroObj2.memoizedResultFunc.clearCache).toBeDefined() - selectorMicroObj2(state) - expect(selectorMicroObj3.fn).toBeDefined() - expect(selectorMicroObj3.cache).toBeDefined() - expect(selectorMicroObj3.isMemoized).toBeDefined() - expect(selectorMicroObj3.options).toBeDefined() - expect(selectorMicroObj3.lastResult).toBeDefined() - expect(selectorMicroObj3.recomputations).toBeDefined() - expect(selectorMicroObj3.dependencies).toBeDefined() - expect(selectorMicroObj3.resetRecomputations).toBeDefined() - expect(selectorMicroObj3.resultFunc).toBeDefined() - // @ts-expect-error - expect(selectorMicroObj3.clearCache).toBeUndefined() - expect(selectorMicroObj3.memoizedResultFunc).toBeDefined() - // @ts-expect-error - expect(selectorMicroObj3.memoizedResultFunc.cache).toBeUndefined() - expect(selectorMicroObj3.memoizedResultFunc.clearCache).toBeDefined() - const selectorLodashObjWithArgsMemoize = - createSelectorMicroObjWithArgsMemoize( - (state: State) => state.todos, - todos => todos.map(t => t.id) - ) + }, - expect(selectorLodashObjWithArgsMemoize.fn).toBeDefined() - expect(selectorLodashObjWithArgsMemoize.cache).toBeDefined() - expect(selectorLodashObjWithArgsMemoize.isMemoized).toBeDefined() - expect(selectorLodashObjWithArgsMemoize.options).toBeDefined() - expect(selectorLodashObjWithArgsMemoize.lastResult).toBeDefined() - expect(selectorLodashObjWithArgsMemoize.recomputations).toBeDefined() - expect(selectorLodashObjWithArgsMemoize.dependencies).toBeDefined() - expect(selectorLodashObjWithArgsMemoize.resetRecomputations).toBeDefined() - expect(selectorLodashObjWithArgsMemoize.resultFunc).toBeDefined() - expect(selectorLodashObjWithArgsMemoize.memoizedResultFunc).toBeDefined() - expect( - selectorLodashObjWithArgsMemoize.memoizedResultFunc.cache - ).toBeDefined() - expect(selectorLodashObjWithArgsMemoize.memoizedResultFunc.fn).toBeDefined() - expect( - selectorLodashObjWithArgsMemoize.memoizedResultFunc.isMemoized - ).toBeDefined() - expect( - selectorLodashObjWithArgsMemoize.memoizedResultFunc.options - ).toBeDefined() + addTodo: (state, action: PayloadAction) => { + const newTodo = { id: action.payload, completed: false } + state.todos.push(newTodo) + } + } +}) - expect(selectorMicroObj.lastResult).toBeDefined() - expect(selectorMicroObj.recomputations).toBeDefined() - expect(selectorMicroObj.dependencies).toBeDefined() - expect(selectorMicroObj.resetRecomputations).toBeDefined() - expect(selectorMicroObj.resultFunc).toBeDefined() - expect(selectorMicroObj.memoizedResultFunc.cache).toBeDefined() - // @ts-expect-error - expect(selectorMicroObj.cache).toBeUndefined() - expect(selectorMicroObj.clearCache).toBeDefined() +const store = configureStore({ + reducer: todoSlice.reducer +}) + +const setupStore = () => + configureStore({ + reducer: todoSlice.reducer + }) + +type LocalTestContext = Record<'store', typeof store> + +describe('argsMemoize and memoize', it => { + beforeEach(context => { + const store = setupStore() + context.store = store + }) + + it('passing memoize directly to createSelector', ({ store }) => { + const state = store.getState() + const selectorDefault = createSelector( + (state: TodoState) => state.todos, + todos => todos.map(({ id }) => id), + { memoize: defaultMemoize } + ) + const selectorDefaultParametric = createSelector( + (state: TodoState, id: number) => id, + (state: TodoState) => state.todos, + (id, todos) => todos.filter(todo => todo.id === id), + { memoize: defaultMemoize } + ) + selectorDefaultParametric(state, 0) + selectorDefaultParametric(state, 1) + selectorDefaultParametric(state, 1) + selectorDefaultParametric(deepClone(state), 1) + selectorDefaultParametric(deepClone(state), 0) + selectorDefaultParametric(deepClone(state), 0) - // @ts-expect-error - expect(selectorMicroObj.cache).toBeUndefined() - // @ts-expect-error - expect(selectorLodashObjWithArgsMemoize.clearCache).toBeUndefined() - expect(selectorLodashObjWithArgsMemoize.cache).toBeDefined() - expect(selectorLodashFunc.clearCache).toBeDefined() - // @ts-expect-error - expect(selectorLodashFunc.cache).toBeUndefined() const selectorAutotrack = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id), + (state: TodoState) => state.todos, + todos => todos.map(({ id }) => id), { memoize: autotrackMemoize } ) - const keys = [ + const outPutSelectorFields: (keyof OutputSelectorFields)[] = [ + 'memoize', + 'argsMemoize', 'resultFunc', 'memoizedResultFunc', - 'dependencies', 'lastResult', + 'dependencies', 'recomputations', - 'resetRecomputations', - 'clearCache' + 'resetRecomputations' ] - expect(selectorDefault).to.include.all.keys(keys) - expect(selectorAutotrack).to.include.all.keys(keys) + const memoizerFields: Exclude< + keyof OutputSelector, + keyof OutputSelectorFields + >[] = ['clearCache'] + const allFields: (keyof OutputSelector)[] = [ + ...outPutSelectorFields, + ...memoizerFields + ] + const hasUndefinedValues = (object: object) => { + return Object.values(object).some(e => e == null) + } + const isMemoizedSelector = (selector: object) => { + return ( + typeof selector === 'function' && + 'resultFunc' in selector && + 'memoizedResultFunc' in selector && + 'lastResult' in selector && + 'dependencies' in selector && + 'recomputations' in selector && + 'resetRecomputations' in selector && + 'memoize' in selector && + 'argsMemoize' in selector && + typeof selector.resultFunc === 'function' && + typeof selector.memoizedResultFunc === 'function' && + typeof selector.lastResult === 'function' && + Array.isArray(selector.dependencies) && + typeof selector.recomputations === 'function' && + typeof selector.resetRecomputations === 'function' && + typeof selector.memoize === 'function' && + typeof selector.argsMemoize === 'function' && + selector.dependencies.length >= 1 && + selector.dependencies.every( + (dependency): dependency is Function => + typeof dependency === 'function' + ) && + !selector.lastResult.length && + !selector.recomputations.length && + !selector.resetRecomputations.length && + typeof selector.recomputations() === 'number' + ) + } + const isArrayOfFunctions = (array: any[]) => + array.every(e => typeof e === 'function') + expect(selectorDefault).toSatisfy(isMemoizedSelector) + expect(selectorDefault) + .to.be.a('function') + .that.has.all.keys(allFields) + .and.satisfies(isMemoizedSelector) + .and.has.own.property('clearCache') + .that.is.a('function') + .with.lengthOf(0) + expect(selectorAutotrack).to.be.a('function').that.has.all.keys(allFields) + expect(selectorDefault.resultFunc).to.be.a('function') + expect(selectorDefault.memoizedResultFunc).to.be.a('function') + 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.resetRecomputations).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') expect(selectorDefault.lastResult()).toBeUndefined() expect(selectorAutotrack.lastResult()).toBeUndefined() expect(selectorDefault.recomputations()).toBe(0) @@ -713,62 +588,34 @@ describe('Customizing selectors', () => { expect(selectorAutotrack(state)).toStrictEqual([0, 1]) expect(selectorDefault.recomputations()).toBe(1) expect(selectorAutotrack.recomputations()).toBe(1) - selectorDefault({ - todos: [ - { id: 0, completed: false }, - { id: 1, completed: false } - ] - }) + selectorDefault(deepClone(state)) const defaultSelectorLastResult1 = selectorDefault.lastResult() - selectorDefault({ - todos: [ - { id: 0, completed: true }, - { id: 1, completed: true } - ] - }) + selectorDefault(deepClone(state)) const defaultSelectorLastResult2 = selectorDefault.lastResult() - selectorAutotrack({ - todos: [ - { id: 0, completed: false }, - { id: 1, completed: false } - ] - }) + selectorAutotrack(deepClone(state)) const autotrackSelectorLastResult1 = selectorAutotrack.lastResult() - selectorAutotrack({ - todos: [ - { id: 0, completed: true }, // flipping completed flag does not cause the autotrack memoizer to re-run. - { id: 1, completed: true } - ] - }) + store.dispatch(todoSlice.actions.toggleCompleted(0)) // flipping completed flag does not cause the autotrack memoizer to re-run. + selectorAutotrack(store.getState()) const autotrackSelectorLastResult2 = selectorAutotrack.lastResult() expect(selectorDefault.recomputations()).toBe(3) expect(selectorAutotrack.recomputations()).toBe(1) expect(autotrackSelectorLastResult1).toBe(autotrackSelectorLastResult2) expect(defaultSelectorLastResult1).not.toBe(defaultSelectorLastResult2) // Default memoize does not preserve referential equality but autotrack does. expect(defaultSelectorLastResult1).toStrictEqual(defaultSelectorLastResult2) + store.dispatch(todoSlice.actions.addTodo(2)) + selectorAutotrack(store.getState()) + expect(selectorAutotrack.recomputations()).toBe(2) }) - test('passing argsMemoize directly to createSelector', () => { - interface State { - todos: { - id: number - completed: boolean - }[] - } - const state: State = { - todos: [ - { id: 0, completed: false }, - { id: 1, completed: false } - ] - } + it('passing argsMemoize directly to createSelector', ({ store }) => { + const state = store.getState() const otherCreateSelector = createSelectorCreator({ memoize: microMemoize, argsMemoize: microMemoize }) - // Overriding back to default const selectorDefault = otherCreateSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id), + (state: TodoState) => state.todos, + todos => todos.map(({ id }) => id), { memoize: defaultMemoize, argsMemoize: defaultMemoize, @@ -783,8 +630,8 @@ describe('Customizing selectors', () => { } ) const selectorAutotrack = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id) + (state: TodoState) => state.todos, + todos => todos.map(({ id }) => id) ) expect(selectorDefault({ ...state })).toStrictEqual([0, 1]) expect(selectorAutotrack({ ...state })).toStrictEqual([0, 1]) @@ -825,21 +672,77 @@ describe('Customizing selectors', () => { expect(autotrackSelectorLastResult1).not.toBe(autotrackSelectorLastResult2) expect(defaultSelectorLastResult1).not.toBe(defaultSelectorLastResult2) expect(defaultSelectorLastResult1).toStrictEqual(defaultSelectorLastResult2) - }) - test('passing argsMemoize to createSelectorCreator', () => { - 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: TodoState) => state.todos, + todos => todos.map(({ id }) => id), + { + memoizeOptions: { resultEqualityCheck: (a, b) => a === b } + } + ) + selectorOriginal(state) + const start = performance.now() + for (let i = 0; i < 1_000_000_0; i++) { + selectorOriginal(state) } + const totalTime = performance.now() - start + expect(totalTime).toBeLessThan(1000) + // Call with new reference to force the selector to re-run + selectorOriginal(deepClone(state)) + selectorOriginal(deepClone(state)) + // Override `argsMemoize` with `autotrackMemoize` + const selectorOverrideArgsMemoize = createSelector( + (state: TodoState) => state.todos, + todos => todos.map(({ id }) => id), + { + memoize: defaultMemoize, + memoizeOptions: { equalityCheck: (a, b) => a === b }, + // WARNING!! This is just for testing purposes, do not use `autotrackMemoize` to memoize the arguments, + // it can return false positives, since it's not tracking a nested field. + argsMemoize: autotrackMemoize + } + ) + selectorOverrideArgsMemoize(state) + // Call with new reference to force the selector to re-run + selectorOverrideArgsMemoize(deepClone(state)) + selectorOverrideArgsMemoize(deepClone(state)) + expect(selectorOverrideArgsMemoize.recomputations()).toBe(1) + expect(selectorOriginal.recomputations()).toBe(3) + const selectorDefaultParametric = createSelector( + (state: TodoState, id: number) => id, + (state: TodoState) => state.todos, + (id, todos) => todos.filter(todo => todo.id === id) + ) + selectorDefaultParametric(state, 1) + selectorDefaultParametric(state, 1) + expect(selectorDefaultParametric.recomputations()).toBe(1) + selectorDefaultParametric(state, 2) + selectorDefaultParametric(state, 1) + expect(selectorDefaultParametric.recomputations()).toBe(3) + selectorDefaultParametric(state, 2) + expect(selectorDefaultParametric.recomputations()).toBe(4) + const selectorDefaultParametricArgsWeakMap = createSelector( + (state: TodoState, id: number) => id, + (state: TodoState) => state.todos, + (id, todos) => todos.filter(todo => todo.id === id), + { argsMemoize: weakMapMemoize } + ) + selectorDefaultParametricArgsWeakMap(state, 1) + selectorDefaultParametricArgsWeakMap(state, 1) + expect(selectorDefaultParametricArgsWeakMap.recomputations()).toBe(1) + selectorDefaultParametricArgsWeakMap(state, 2) + selectorDefaultParametricArgsWeakMap(state, 1) + expect(selectorDefaultParametricArgsWeakMap.recomputations()).toBe(2) + selectorDefaultParametricArgsWeakMap(state, 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) + }) + + it('passing argsMemoize to createSelectorCreator', ({ store }) => { + const state = store.getState() const createSelectorMicroMemoize = createSelectorCreator({ memoize: microMemoize, memoizeOptions: { isEqual: (a, b) => a === b }, @@ -847,42 +750,43 @@ describe('Customizing selectors', () => { argsMemoizeOptions: { isEqual: (a, b) => a === b } }) const selectorMicroMemoize = createSelectorMicroMemoize( - (state: State) => state.todos, - todos => todos.map(t => t.id) + (state: TodoState) => state.todos, + todos => todos.map(({ id }) => id) ) - expect(selectorMicroMemoize(state)).toBeDefined() + expect(selectorMicroMemoize(state)).to.be.an('array').that.is.not.empty // Checking existence of fields related to `argsMemoize` - expect(selectorMicroMemoize.cache).toBeDefined() - expect(selectorMicroMemoize.fn).toBeDefined() - expect(selectorMicroMemoize.isMemoized).toBeDefined() - expect(selectorMicroMemoize.options).toBeDefined() + expect(selectorMicroMemoize.cache).to.be.an('object') + expect(selectorMicroMemoize.fn).to.be.a('function') + expect(selectorMicroMemoize.isMemoized).to.be.true + expect(selectorMicroMemoize.options).to.be.an('object') // @ts-expect-error expect(selectorMicroMemoize.clearCache).toBeUndefined() + expect(selectorMicroMemoize.memoizedResultFunc).to.be.a('function') // Checking existence of fields related to `memoize` - expect(selectorMicroMemoize.memoizedResultFunc.cache).toBeDefined() - expect(selectorMicroMemoize.memoizedResultFunc.fn).toBeDefined() - expect(selectorMicroMemoize.memoizedResultFunc.isMemoized).toBeDefined() - expect(selectorMicroMemoize.memoizedResultFunc.options).toBeDefined() + expect(selectorMicroMemoize.memoizedResultFunc.cache).to.be.an('object') + expect(selectorMicroMemoize.memoizedResultFunc.fn).to.be.a('function') + expect(selectorMicroMemoize.memoizedResultFunc.isMemoized).to.be.true + expect(selectorMicroMemoize.memoizedResultFunc.options).to.be.an('object') // @ts-expect-error expect(selectorMicroMemoize.memoizedResultFunc.clearCache).toBeUndefined() - expect(selectorMicroMemoize.memoizedResultFunc).toBeDefined() // Checking existence of fields related to the actual memoized selector - expect(selectorMicroMemoize.dependencies).toBeDefined() - expect(selectorMicroMemoize.lastResult()).toBeDefined() + expect(selectorMicroMemoize.dependencies).to.be.an('array').that.is.not + .empty + expect(selectorMicroMemoize.lastResult()).to.be.an('array').that.is.not + .empty expect( selectorMicroMemoize.memoizedResultFunc([{ id: 0, completed: true }]) - ).toBeDefined() - expect(selectorMicroMemoize.recomputations()).toBeDefined() - expect(selectorMicroMemoize.resetRecomputations()).toBeDefined() - expect(selectorMicroMemoize.resultFunc).toBeDefined() + ).to.be.an('array').that.is.not.empty + expect(selectorMicroMemoize.recomputations()).to.be.a('number') + expect(selectorMicroMemoize.resetRecomputations()).toBe(0) + expect(selectorMicroMemoize.resultFunc).to.be.a('function') expect( selectorMicroMemoize.resultFunc([{ id: 0, completed: true }]) - ).toBeDefined() + ).to.be.an('array').that.is.not.empty - // Checking to see if types dynamically change if memoize or argsMemoize or overridden inside `createSelector` const selectorMicroMemoizeOverridden = createSelectorMicroMemoize( - (state: State) => state.todos, - todos => todos.map(t => t.id), + (state: TodoState) => state.todos, + todos => todos.map(({ id }) => id), { memoize: defaultMemoize, argsMemoize: defaultMemoize, @@ -890,9 +794,10 @@ describe('Customizing selectors', () => { argsMemoizeOptions: { equalityCheck: (a, b) => a === b, maxSize: 3 } } ) - expect(selectorMicroMemoizeOverridden(state)).toBeDefined() + expect(selectorMicroMemoizeOverridden(state)).to.be.an('array').that.is.not + .empty // Checking existence of fields related to `argsMemoize` - expect(selectorMicroMemoizeOverridden.clearCache).toBeDefined() + expect(selectorMicroMemoizeOverridden.clearCache).to.be.a('function') // @ts-expect-error expect(selectorMicroMemoizeOverridden.cache).toBeUndefined() // @ts-expect-error @@ -904,7 +809,7 @@ describe('Customizing selectors', () => { // Checking existence of fields related to `memoize` expect( selectorMicroMemoizeOverridden.memoizedResultFunc.clearCache - ).toBeDefined() + ).to.be.a('function') expect( // @ts-expect-error selectorMicroMemoizeOverridden.memoizedResultFunc.cache @@ -920,33 +825,36 @@ describe('Customizing selectors', () => { selectorMicroMemoizeOverridden.memoizedResultFunc.options ).toBeUndefined() // Checking existence of fields related to the actual memoized selector - expect(selectorMicroMemoizeOverridden.dependencies).toBeDefined() - expect(selectorMicroMemoizeOverridden.lastResult()).toBeDefined() + expect(selectorMicroMemoizeOverridden.dependencies).to.be.an('array').that + .is.not.empty + expect(selectorMicroMemoizeOverridden.lastResult()).to.be.an('array').that + .is.not.empty expect( selectorMicroMemoizeOverridden.memoizedResultFunc([ { id: 0, completed: true } ]) - ).toBeDefined() - expect(selectorMicroMemoizeOverridden.memoizedResultFunc).toBeDefined() - expect(selectorMicroMemoizeOverridden.recomputations()).toBeDefined() - expect(selectorMicroMemoizeOverridden.resetRecomputations()).toBeDefined() - expect(selectorMicroMemoizeOverridden.resultFunc).toBeDefined() + ).to.be.an('array').that.is.not.empty + expect(selectorMicroMemoizeOverridden.recomputations()).to.be.a('number') + expect(selectorMicroMemoizeOverridden.resetRecomputations()).toBe(0) expect( selectorMicroMemoizeOverridden.resultFunc([{ id: 0, completed: true }]) - ).toBeDefined() + ).to.be.an('array').that.is.not.empty const selectorMicroMemoizeOverrideArgsMemoizeOnly = createSelectorMicroMemoize( - (state: State) => state.todos, - todos => todos.map(t => t.id), + (state: TodoState) => state.todos, + todos => todos.map(({ id }) => id), { argsMemoize: defaultMemoize, argsMemoizeOptions: { resultEqualityCheck: (a, b) => a === b } } ) - expect(selectorMicroMemoizeOverrideArgsMemoizeOnly(state)).toBeDefined() + expect(selectorMicroMemoizeOverrideArgsMemoizeOnly(state)).to.be.an('array') + .that.is.not.empty // Checking existence of fields related to `argsMemoize` - expect(selectorMicroMemoizeOverrideArgsMemoizeOnly.clearCache).toBeDefined() + expect(selectorMicroMemoizeOverrideArgsMemoizeOnly.clearCache).to.be.a( + 'function' + ) // @ts-expect-error expect(selectorMicroMemoizeOverrideArgsMemoizeOnly.cache).toBeUndefined() // @ts-expect-error @@ -965,274 +873,113 @@ describe('Customizing selectors', () => { ).toBeUndefined() expect( selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc.cache - ).toBeDefined() + ).to.be.a('object') expect( selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc.fn - ).toBeDefined() + ).to.be.a('function') expect( selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc.isMemoized - ).toBeDefined() + ).to.be.true expect( selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc.options - ).toBeDefined() + ).to.be.a('object') // Checking existence of fields related to the actual memoized selector - expect( - selectorMicroMemoizeOverrideArgsMemoizeOnly.dependencies - ).toBeDefined() - expect( - selectorMicroMemoizeOverrideArgsMemoizeOnly.lastResult() - ).toBeDefined() + expect(selectorMicroMemoizeOverrideArgsMemoizeOnly.dependencies).to.be.an( + 'array' + ).that.is.not.empty + expect(selectorMicroMemoizeOverrideArgsMemoizeOnly.lastResult()).to.be.an( + 'array' + ).that.is.not.empty expect( selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc([ { id: 0, completed: true } ]) - ).toBeDefined() - expect( - selectorMicroMemoizeOverrideArgsMemoizeOnly.memoizedResultFunc - ).toBeDefined() + ).to.be.an('array').that.is.not.empty expect( selectorMicroMemoizeOverrideArgsMemoizeOnly.recomputations() - ).toBeDefined() + ).to.be.a('number') expect( selectorMicroMemoizeOverrideArgsMemoizeOnly.resetRecomputations() - ).toBeDefined() - expect(selectorMicroMemoizeOverrideArgsMemoizeOnly.resultFunc).toBeDefined() + ).toBe(0) expect( selectorMicroMemoizeOverrideArgsMemoizeOnly.resultFunc([ { id: 0, completed: true } ]) - ).toBeDefined() + ).to.be.an('array').that.is.not.empty const selectorMicroMemoizeOverrideMemoizeOnly = createSelectorMicroMemoize( - (state: State) => state.todos, - todos => todos.map(t => t.id), + (state: TodoState) => state.todos, + todos => todos.map(({ id }) => id), { memoize: defaultMemoize, memoizeOptions: { resultEqualityCheck: (a, b) => a === b } } ) - expect(selectorMicroMemoizeOverrideMemoizeOnly(state)).toBeDefined() + expect(selectorMicroMemoizeOverrideMemoizeOnly(state)).to.be.an('array') + .that.is.not.empty // Checking existence of fields related to `argsMemoize` // @ts-expect-error Note that since we did not override `argsMemoize` in the options object, // selector.clearCache becomes an invalid field access, and we get `cache`, `fn`, `isMemoized` and `options` instead. expect(selectorMicroMemoizeOverrideMemoizeOnly.clearCache).toBeUndefined() - expect(selectorMicroMemoizeOverrideMemoizeOnly.cache).toBeDefined() - expect(selectorMicroMemoizeOverrideMemoizeOnly.fn).toBeDefined() - expect(selectorMicroMemoizeOverrideMemoizeOnly.isMemoized).toBeDefined() - expect(selectorMicroMemoizeOverrideMemoizeOnly.options).toBeDefined() + expect(selectorMicroMemoizeOverrideMemoizeOnly).to.have.all.keys([ + 'cache', + 'fn', + 'isMemoized', + 'options', + 'resultFunc', + 'memoizedResultFunc', + 'lastResult', + 'dependencies', + 'recomputations', + 'resetRecomputations', + 'memoize', + 'argsMemoize' + ]) + expect(selectorMicroMemoizeOverrideMemoizeOnly.cache).to.be.an('object') + expect(selectorMicroMemoizeOverrideMemoizeOnly.fn).to.be.a('function') + expect(selectorMicroMemoizeOverrideMemoizeOnly.isMemoized).to.be.true + expect(selectorMicroMemoizeOverrideMemoizeOnly.options).to.be.an('object') // Checking existence of fields related to `memoize` - expect( - // @ts-expect-error - selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc.cache - ).toBeUndefined() - expect( - // @ts-expect-error - selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc.fn - ).toBeUndefined() - expect( - // @ts-expect-error - selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc.isMemoized - ).toBeUndefined() - expect( - // @ts-expect-error - selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc.options - ).toBeUndefined() + expect(selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc) + .to.be.a('function') + .that.has.all.keys(['clearCache']) expect( selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc.clearCache - ).toBeDefined() + ).to.be.a('function') // Checking existence of fields related to the actual memoized selector - expect(selectorMicroMemoizeOverrideMemoizeOnly.dependencies).toBeDefined() - expect(selectorMicroMemoizeOverrideMemoizeOnly.lastResult()).toBeDefined() + expect(selectorMicroMemoizeOverrideMemoizeOnly.dependencies).to.be.an( + 'array' + ).that.is.not.empty + expect(selectorMicroMemoizeOverrideMemoizeOnly.lastResult()).to.be.an( + 'array' + ).that.is.not.empty expect( selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc([ { id: 0, completed: true } ]) - ).toBeDefined() - expect( - selectorMicroMemoizeOverrideMemoizeOnly.memoizedResultFunc - ).toBeDefined() - expect( - selectorMicroMemoizeOverrideMemoizeOnly.recomputations() - ).toBeDefined() - expect( - selectorMicroMemoizeOverrideMemoizeOnly.resetRecomputations() - ).toBeDefined() - expect(selectorMicroMemoizeOverrideMemoizeOnly.resultFunc).toBeDefined() + ).to.be.an('array').that.is.not.empty + expect(selectorMicroMemoizeOverrideMemoizeOnly.recomputations()).to.be.a( + 'number' + ) + expect(selectorMicroMemoizeOverrideMemoizeOnly.resetRecomputations()).toBe( + 0 + ) expect( selectorMicroMemoizeOverrideMemoizeOnly.resultFunc([ { id: 0, completed: true } ]) - ).toBeDefined() - // If we don't pass in `argsMemoize`, the type for `argsMemoizeOptions` falls back to the options parameter of `defaultMemoize`. - const createSelectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault = - createSelectorCreator({ - memoize: microMemoize, - memoizeOptions: { isPromise: false }, - argsMemoizeOptions: { resultEqualityCheck: (a, b) => a === b } - }) - const selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault = - createSelectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault( - (state: State) => state.todos, - todos => todos.map(t => t.id) - ) - expect( - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault(state) - ).toBeDefined() - expect( - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.clearCache - ).toBeDefined() - expect( - // @ts-expect-error - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.cache - ).toBeUndefined() - expect( - // @ts-expect-error - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.fn - ).toBeUndefined() - expect( - // @ts-expect-error - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.isMemoized - ).toBeUndefined() - expect( - // @ts-expect-error - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.options - ).toBeUndefined() - // Checking existence of fields related to `memoize` - expect( - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc - .cache - ).toBeDefined() - expect( - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc - .fn - ).toBeDefined() - expect( - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc - .isMemoized - ).toBeDefined() - expect( - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc - .options - ).toBeDefined() - expect( - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.dependencies - ).toBeDefined() - expect( - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.lastResult() - ).toBeDefined() - expect( - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc( - [{ id: 0, completed: true }] - ) - ).toBeDefined() - expect( - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.memoizedResultFunc - ).toBeDefined() - expect( - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.recomputations() - ).toBeDefined() - expect( - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.resetRecomputations() - ).toBeDefined() - expect( - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.resultFunc - ).toBeDefined() - expect( - selectorMicroMemoizeArgsMemoizeOptionsFallbackToDefault.resultFunc([ - { id: 0, completed: true } - ]) - ).toBeDefined() + ).to.be.an('array').that.is.not.empty }) - test('passing argsMemoize directly to createSelector', () => { - 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(t => t.id) - ) - selectorOriginal(state) - // Call with new reference to force the selector to re-run - selectorOriginal({ - todos: [ - { id: 0, completed: false }, - { id: 1, completed: false } - ] - }) - selectorOriginal({ - todos: [ - { id: 0, completed: false }, - { id: 1, completed: false } - ] - }) - // Override `argsMemoize` with `autotrackMemoize` - const selectorOverrideArgsMemoize = createSelector( - (state: State) => state.todos, - todos => todos.map(t => t.id), - { - memoize: defaultMemoize, - memoizeOptions: { equalityCheck: (a, b) => a === b }, - // WARNING!! This is just for testing purposes, do not use `autotrackMemoize` to memoize the arguments, - // it can return false positives, since it's not tracking a nested field. - argsMemoize: autotrackMemoize - } - ) - selectorOverrideArgsMemoize(state) - // Call with new reference to force the selector to re-run - selectorOverrideArgsMemoize({ - todos: [ - { id: 0, completed: false }, - { id: 1, completed: false } - ] - }) - selectorOverrideArgsMemoize({ - todos: [ - { id: 0, completed: false }, - { id: 1, completed: false } - ] + it('pass options object to createSelectorCreator ', ({ store }) => { + const state = store.getState() + const createSelectorMicro = createSelectorCreator({ + memoize: microMemoize, + memoizeOptions: { isEqual: (a, b) => a === b } }) - expect(selectorOverrideArgsMemoize.recomputations()).toBe(1) - expect(selectorOriginal.recomputations()).toBe(3) - 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) - expect(selectorDefaultParametric.recomputations()).toBe(1) - selectorDefaultParametric(state, 2) - selectorDefaultParametric(state, 1) - expect(selectorDefaultParametric.recomputations()).toBe(3) - selectorDefaultParametric(state, 2) - expect(selectorDefaultParametric.recomputations()).toBe(4) - const selectorDefaultParametricArgsWeakMap = createSelector( - (state: State, id: number) => id, - (state: State) => state.todos, - (id, todos) => todos.filter(todo => todo.id === id), - { argsMemoize: weakMapMemoize } + const selectorMicro = createSelectorMicro( + (state: TodoState) => state.todos, + todos => todos.map(({ id }) => id) ) - selectorDefaultParametricArgsWeakMap(state, 1) - selectorDefaultParametricArgsWeakMap(state, 1) - expect(selectorDefaultParametricArgsWeakMap.recomputations()).toBe(1) - selectorDefaultParametricArgsWeakMap(state, 2) - selectorDefaultParametricArgsWeakMap(state, 1) - expect(selectorDefaultParametricArgsWeakMap.recomputations()).toBe(2) - selectorDefaultParametricArgsWeakMap(state, 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) }) })