From 6cf53cbe059f4306c69eb1edd5fa318b44610e5c Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sun, 26 Nov 2023 02:00:28 -0600 Subject: [PATCH 01/24] Add initial implementation for `noopCheck` --- src/createSelectorCreator.ts | 20 +++++- src/index.ts | 1 + src/types.ts | 2 + src/utils.ts | 19 ++++++ test/defaultMemoize.spec.ts | 24 +++---- test/noopCheck.test.ts | 127 +++++++++++++++++++++++++++++++++++ test/reselect.spec.ts | 4 +- test/testUtils.ts | 8 +-- test/weakmapMemoize.spec.ts | 4 +- 9 files changed, 185 insertions(+), 24 deletions(-) create mode 100644 test/noopCheck.test.ts diff --git a/src/createSelectorCreator.ts b/src/createSelectorCreator.ts index 51ee1030d..d99a5c645 100644 --- a/src/createSelectorCreator.ts +++ b/src/createSelectorCreator.ts @@ -22,6 +22,7 @@ import { collectInputSelectorResults, ensureIsArray, getDependencies, + runNoopCheck, runStabilityCheck, shouldRunInputStabilityCheck } from './utils' @@ -187,6 +188,14 @@ export function setInputStabilityCheckEnabled( globalStabilityCheck = inputStabilityCheckFrequency } +let globalNoopCheck: StabilityCheckFrequency = 'once' + +export const setGlobalNoopCheck = ( + noopCheckFrequency: StabilityCheckFrequency +) => { + globalNoopCheck = noopCheckFrequency +} + /** * Creates a selector creator function with the specified memoization function and options for customizing memoization behavior. * @@ -374,7 +383,8 @@ export function createSelectorCreator< memoizeOptions = [], argsMemoize = weakMapMemoize, argsMemoizeOptions = [], - inputStabilityCheck = globalStabilityCheck + inputStabilityCheck = globalStabilityCheck, + noopCheck = globalNoopCheck } = combinedOptions // Simplifying assumption: it's unlikely that the first options arg of the provided memoizer @@ -408,6 +418,14 @@ export function createSelectorCreator< arguments ) + if ( + process.env.NODE_ENV !== 'production' && + inputSelectorResults.length && + shouldRunInputStabilityCheck(noopCheck, firstRun) + ) { + runNoopCheck(resultFunc as Combiner) + } + if ( process.env.NODE_ENV !== 'production' && shouldRunInputStabilityCheck(inputStabilityCheck, firstRun) diff --git a/src/index.ts b/src/index.ts index 6a85cb051..cf52fc80d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ export { autotrackMemoize as unstable_autotrackMemoize } from './autotrackMemoiz export { createSelector, createSelectorCreator, + setGlobalNoopCheck, setInputStabilityCheckEnabled } from './createSelectorCreator' export type { CreateSelectorFunction } from './createSelectorCreator' diff --git a/src/types.ts b/src/types.ts index a28640814..ba3bfc878 100644 --- a/src/types.ts +++ b/src/types.ts @@ -83,6 +83,8 @@ export interface CreateSelectorOptions< */ inputStabilityCheck?: StabilityCheckFrequency + noopCheck?: StabilityCheckFrequency + /** * The memoize function that is used to memoize the {@linkcode OutputSelectorFields.resultFunc resultFunc} * inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). diff --git a/src/utils.ts b/src/utils.ts index b49b92282..42b60a0db 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ import type { + AnyFunction, CreateSelectorOptions, Selector, SelectorArray, @@ -167,6 +168,24 @@ export function runStabilityCheck( } } +export const runNoopCheck = (resultFunc: Func) => { + let isInputSameAsOutput = false + try { + const emptyObject = {} + if (resultFunc(emptyObject) === emptyObject) isInputSameAsOutput = true + } catch { + // Do nothing + } + if (isInputSameAsOutput) { + console.warn( + 'The result function returned its own inputs without modification. e.g' + + '\n`createSelector([state => state.todos], todos => todos)`' + + '\nThis could lead to inefficient memoization and unnecessary re-renders.' + + '\nEnsure transformation logic is in the result function, and extraction logic is in the input selectors.' + ) + } +} + /** * Determines if the input stability check should run. * diff --git a/test/defaultMemoize.spec.ts b/test/defaultMemoize.spec.ts index 7d3237f62..54eb9124e 100644 --- a/test/defaultMemoize.spec.ts +++ b/test/defaultMemoize.spec.ts @@ -274,12 +274,12 @@ describe('defaultMemoize', () => { ) fooChangeHandler(state) - expect(fooChangeSpy.mock.calls.length).toEqual(1) + expect(fooChangeSpy.mock.calls.length).toEqual(2) // no change fooChangeHandler(state) // this would fail - expect(fooChangeSpy.mock.calls.length).toEqual(1) + expect(fooChangeSpy.mock.calls.length).toEqual(2) const state2 = { a: 1 } let count = 0 @@ -290,9 +290,9 @@ describe('defaultMemoize', () => { }) selector(state) - expect(count).toBe(1) + expect(count).toBe(2) selector(state) - expect(count).toBe(1) + expect(count).toBe(2) }) test('Accepts an options object as an arg', () => { @@ -374,33 +374,33 @@ describe('defaultMemoize', () => { // Initial call selector('a') // ['a'] - expect(funcCalls).toBe(1) + expect(funcCalls).toBe(2) // In cache - memoized selector('a') // ['a'] - expect(funcCalls).toBe(1) + expect(funcCalls).toBe(2) // Added selector('b') // ['b', 'a'] - expect(funcCalls).toBe(2) + expect(funcCalls).toBe(3) // Added selector('c') // ['c', 'b', 'a'] - expect(funcCalls).toBe(3) + expect(funcCalls).toBe(4) // Already in cache selector('c') // ['c', 'b', 'a'] - expect(funcCalls).toBe(3) + expect(funcCalls).toBe(4) selector.memoizedResultFunc.clearCache() // Added selector('a') // ['a'] - expect(funcCalls).toBe(4) + expect(funcCalls).toBe(5) // Already in cache selector('a') // ['a'] - expect(funcCalls).toBe(4) + expect(funcCalls).toBe(5) // make sure clearCache is passed to the selector correctly selector.clearCache() @@ -409,7 +409,7 @@ describe('defaultMemoize', () => { // Note: the outer arguments wrapper function still has 'a' in its own size-1 cache, so passing // 'a' here would _not_ recalculate selector('b') // ['b'] - expect(funcCalls).toBe(5) + expect(funcCalls).toBe(6) try { //@ts-expect-error issue 591 diff --git a/test/noopCheck.test.ts b/test/noopCheck.test.ts new file mode 100644 index 000000000..f5771ba4d --- /dev/null +++ b/test/noopCheck.test.ts @@ -0,0 +1,127 @@ +import { createSelector, setGlobalNoopCheck } from 'reselect' +import type { LocalTestContext, RootState } from './testUtils' +import { localTest } from './testUtils' + +describe('noopCheck', () => { + const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + const identityFunction = vi.fn((state: T) => state) + const badSelector = createSelector( + [(state: RootState) => state], + identityFunction + ) + + afterEach(() => { + consoleSpy.mockClear() + identityFunction.mockClear() + badSelector.clearCache() + badSelector.memoizedResultFunc.clearCache() + }) + afterAll(() => { + consoleSpy.mockRestore() + }) + localTest( + 'calls the result function twice, and warns to console if result is the same as argument', + ({ state }) => { + const goodSelector = createSelector( + [(state: RootState) => state], + state => state.todos + ) + + expect(goodSelector(state)).toBe(state.todos) + + expect(consoleSpy).not.toHaveBeenCalled() + + expect(badSelector(state)).toBe(state) + + expect(identityFunction).toHaveBeenCalledTimes(2) + + expect(consoleSpy).toHaveBeenCalled() + } + ) + + localTest('disables check if global setting is set to never', ({ state }) => { + setGlobalNoopCheck('never') + + expect(badSelector(state)).toBe(state) + + expect(identityFunction).toHaveBeenCalledTimes(1) + + expect(consoleSpy).not.toHaveBeenCalled() + + setGlobalNoopCheck('once') + }) + + localTest( + 'disables check if specified in the selector options', + ({ state }) => { + const badSelector = createSelector( + [(state: RootState) => state], + identityFunction, + { noopCheck: 'never' } + ) + + expect(badSelector(state)).toBe(state) + + expect(identityFunction).toHaveBeenCalledTimes(1) + + expect(consoleSpy).not.toHaveBeenCalled() + } + ) + + localTest('disables check in production', ({ state }) => { + const originalEnv = process.env.NODE_ENV + process.env.NODE_ENV = 'production' + + expect(badSelector(state)).toBe(state) + + expect(identityFunction).toHaveBeenCalledTimes(1) + + expect(consoleSpy).not.toHaveBeenCalled() + + process.env.NODE_ENV = originalEnv + }) + + localTest('allows running the check only once', ({ state }) => { + const badSelector = createSelector( + [(state: RootState) => state], + identityFunction, + { noopCheck: 'once' } + ) + expect(badSelector(state)).toBe(state) + + expect(identityFunction).toHaveBeenCalledTimes(2) + + expect(consoleSpy).toHaveBeenCalledOnce() + + const newState = { ...state } + + expect(badSelector(newState)).toBe(newState) + + expect(identityFunction).toHaveBeenCalledTimes(3) + + expect(consoleSpy).toHaveBeenCalledOnce() + }) + + localTest('uses the memoize provided', ({ state }) => { + // console.log(setupStore) + // const store = setupStore() + // const state = store.getState() + const badSelector = createSelector( + [(state: RootState) => state.todos], + identityFunction + ) + expect(badSelector(state)).toBe(state.todos) + + expect(identityFunction).toHaveBeenCalledTimes(2) + + expect(consoleSpy).toHaveBeenCalledTimes(1) + + const newState = { ...state } + + expect(badSelector({ ...state })).not.toBe(state) + + // expect(identityFunction).toHaveBeenCalledTimes(3) + + expect(consoleSpy).toHaveBeenCalledTimes(1) + }) +}) diff --git a/test/reselect.spec.ts b/test/reselect.spec.ts index 0f872c62a..5e59520ba 100644 --- a/test/reselect.spec.ts +++ b/test/reselect.spec.ts @@ -214,7 +214,7 @@ describe('Basic selector behavior', () => { ) expect(() => selector({ a: 1 })).toThrow('test error') expect(() => selector({ a: 1 })).toThrow('test error') - expect(called).toBe(2) + expect(called).toBe(3) }) test('memoizes previous result before exception', () => { @@ -232,7 +232,7 @@ describe('Basic selector behavior', () => { expect(selector(state1)).toBe(1) expect(() => selector(state2)).toThrow('test error') expect(selector(state1)).toBe(1) - expect(called).toBe(2) + expect(called).toBe(3) }) }) diff --git a/test/testUtils.ts b/test/testUtils.ts index 12c557f5b..2de37995d 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -1,13 +1,7 @@ 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' +import type { AnyFunction, OutputSelector, Simplify } from '../src/types' export interface Todo { id: number diff --git a/test/weakmapMemoize.spec.ts b/test/weakmapMemoize.spec.ts index 338441e54..309e44086 100644 --- a/test/weakmapMemoize.spec.ts +++ b/test/weakmapMemoize.spec.ts @@ -193,7 +193,7 @@ describe('Basic selector behavior with weakMapMemoize', () => { ) expect(() => selector({ a: 1 })).toThrow('test error') expect(() => selector({ a: 1 })).toThrow('test error') - expect(called).toBe(2) + expect(called).toBe(3) }) test('memoizes previous result before exception', () => { @@ -211,6 +211,6 @@ describe('Basic selector behavior with weakMapMemoize', () => { expect(selector(state1)).toBe(1) expect(() => selector(state2)).toThrow('test error') expect(selector(state1)).toBe(1) - expect(called).toBe(2) + expect(called).toBe(3) }) }) From a8645a057854807e5af20d0f6755c29e14530138 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sun, 26 Nov 2023 03:59:14 -0600 Subject: [PATCH 02/24] Disable no-op check for some unit tests --- test/autotrackMemoize.spec.ts | 6 ++++-- test/defaultMemoize.spec.ts | 29 ++++++++++++----------------- test/reselect.spec.ts | 20 +++++++++++++------- test/selectorUtils.spec.ts | 6 +++++- test/weakmapMemoize.spec.ts | 11 +++++++---- 5 files changed, 41 insertions(+), 31 deletions(-) diff --git a/test/autotrackMemoize.spec.ts b/test/autotrackMemoize.spec.ts index 726f18d16..c21bc301f 100644 --- a/test/autotrackMemoize.spec.ts +++ b/test/autotrackMemoize.spec.ts @@ -33,7 +33,8 @@ describe('Basic selector behavior with autotrack', () => { // console.log('Selector test') const selector = createSelector( (state: StateA) => state.a, - a => a + a => a, + { noopCheck: 'never' } ) const firstState = { a: 1 } const firstStateNewPointer = { a: 1 } @@ -51,7 +52,8 @@ describe('Basic selector behavior with autotrack', () => { test("don't pass extra parameters to inputSelector when only called with the state", () => { const selector = createSelector( (...params: any[]) => params.length, - a => a + a => a, + { noopCheck: 'never' } ) expect(selector({})).toBe(1) }) diff --git a/test/defaultMemoize.spec.ts b/test/defaultMemoize.spec.ts index 54eb9124e..716bec6c1 100644 --- a/test/defaultMemoize.spec.ts +++ b/test/defaultMemoize.spec.ts @@ -368,39 +368,40 @@ describe('defaultMemoize', () => { return state }, { - memoizeOptions: { maxSize: 3 } + memoizeOptions: { maxSize: 3 }, + noopCheck: 'never' } ) // Initial call selector('a') // ['a'] - expect(funcCalls).toBe(2) + expect(funcCalls).toBe(1) // In cache - memoized selector('a') // ['a'] - expect(funcCalls).toBe(2) + expect(funcCalls).toBe(1) // Added selector('b') // ['b', 'a'] - expect(funcCalls).toBe(3) + expect(funcCalls).toBe(2) // Added selector('c') // ['c', 'b', 'a'] - expect(funcCalls).toBe(4) + expect(funcCalls).toBe(3) // Already in cache selector('c') // ['c', 'b', 'a'] - expect(funcCalls).toBe(4) + expect(funcCalls).toBe(3) selector.memoizedResultFunc.clearCache() // Added selector('a') // ['a'] - expect(funcCalls).toBe(5) + expect(funcCalls).toBe(4) // Already in cache selector('a') // ['a'] - expect(funcCalls).toBe(5) + expect(funcCalls).toBe(4) // make sure clearCache is passed to the selector correctly selector.clearCache() @@ -409,14 +410,8 @@ describe('defaultMemoize', () => { // Note: the outer arguments wrapper function still has 'a' in its own size-1 cache, so passing // 'a' here would _not_ recalculate selector('b') // ['b'] - expect(funcCalls).toBe(6) - - try { - //@ts-expect-error issue 591 - selector.resultFunc.clearCache() - fail('should have thrown for issue 591') - } catch (err) { - //expected catch - } + expect(funcCalls).toBe(5) + // @ts-expect-error + expect(selector.resultFunc.clearCache).toBeUndefined() }) }) diff --git a/test/reselect.spec.ts b/test/reselect.spec.ts index 5e59520ba..d7504e726 100644 --- a/test/reselect.spec.ts +++ b/test/reselect.spec.ts @@ -41,7 +41,8 @@ describe('Basic selector behavior', () => { test('basic selector', () => { const selector = createSelector( (state: StateA) => state.a, - a => a + a => a, + { noopCheck: 'never' } ) const firstState = { a: 1 } const firstStateNewPointer = { a: 1 } @@ -59,7 +60,8 @@ describe('Basic selector behavior', () => { test("don't pass extra parameters to inputSelector when only called with the state", () => { const selector = createSelector( (...params: any[]) => params.length, - a => a + a => a, + { noopCheck: 'never' } ) expect(selector({})).toBe(1) }) @@ -164,7 +166,8 @@ describe('Basic selector behavior', () => { test('memoized composite arguments', () => { const selector = createSelector( (state: StateSub) => state.sub, - sub => sub + sub => sub, + { noopCheck: 'never' } ) const state1 = { sub: { a: 1 } } expect(selector(state1)).toEqual({ a: 1 }) @@ -225,14 +228,15 @@ describe('Basic selector behavior', () => { called++ if (a > 1) throw Error('test error') return a - } + }, + { noopCheck: 'never' } ) const state1 = { a: 1 } const state2 = { a: 2 } expect(selector(state1)).toBe(1) expect(() => selector(state2)).toThrow('test error') expect(selector(state1)).toBe(1) - expect(called).toBe(3) + expect(called).toBe(2) }) }) @@ -240,7 +244,8 @@ describe('Combining selectors', () => { test('chained selector', () => { const selector1 = createSelector( (state: StateSub) => state.sub, - sub => sub + sub => sub, + { noopCheck: 'never' } ) const selector2 = createSelector(selector1, sub => sub.a) const state1 = { sub: { a: 1 } } @@ -301,7 +306,8 @@ describe('Combining selectors', () => { ) const selector = createOverridenSelector( (state: StateA) => state.a, - a => a + a => a, + { noopCheck: 'never' } ) expect(selector({ a: 1 })).toBe(1) expect(selector({ a: 2 })).toBe(1) // yes, really true diff --git a/test/selectorUtils.spec.ts b/test/selectorUtils.spec.ts index c493cc71e..526bea33f 100644 --- a/test/selectorUtils.spec.ts +++ b/test/selectorUtils.spec.ts @@ -6,7 +6,11 @@ describe('createSelector exposed utils', () => { const selector = createSelector( (state: StateA) => state.a, a => a, - { memoize: defaultMemoize, argsMemoize: defaultMemoize } + { + memoize: defaultMemoize, + argsMemoize: defaultMemoize, + noopCheck: 'never' + } ) expect(selector({ a: 1 })).toBe(1) expect(selector({ a: 1 })).toBe(1) diff --git a/test/weakmapMemoize.spec.ts b/test/weakmapMemoize.spec.ts index 309e44086..fb66ff23f 100644 --- a/test/weakmapMemoize.spec.ts +++ b/test/weakmapMemoize.spec.ts @@ -30,7 +30,8 @@ describe('Basic selector behavior with weakMapMemoize', () => { // console.log('Selector test') const selector = createSelector( (state: StateA) => state.a, - a => a + a => a, + { noopCheck: 'never' } ) const firstState = { a: 1 } const firstStateNewPointer = { a: 1 } @@ -48,7 +49,8 @@ describe('Basic selector behavior with weakMapMemoize', () => { test("don't pass extra parameters to inputSelector when only called with the state", () => { const selector = createSelector( (...params: any[]) => params.length, - a => a + a => a, + { noopCheck: 'never' } ) expect(selector({})).toBe(1) }) @@ -204,13 +206,14 @@ describe('Basic selector behavior with weakMapMemoize', () => { called++ if (a > 1) throw Error('test error') return a - } + }, + { noopCheck: 'never' } ) const state1 = { a: 1 } const state2 = { a: 2 } expect(selector(state1)).toBe(1) expect(() => selector(state2)).toThrow('test error') expect(selector(state1)).toBe(1) - expect(called).toBe(3) + expect(called).toBe(2) }) }) From 703bf3aa588a77db5fe339536abebe72e8b2c87a Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sun, 26 Nov 2023 04:57:07 -0600 Subject: [PATCH 03/24] Disable no-op check for performance tests --- src/createSelectorCreator.ts | 1 - test/autotrackMemoize.spec.ts | 41 ++-- test/examples.test.ts | 440 +++++++++++++++++----------------- test/reselect.spec.ts | 61 +++-- test/testUtils.ts | 8 + test/weakmapMemoize.spec.ts | 98 ++++---- 6 files changed, 323 insertions(+), 326 deletions(-) diff --git a/src/createSelectorCreator.ts b/src/createSelectorCreator.ts index d99a5c645..ee10fe798 100644 --- a/src/createSelectorCreator.ts +++ b/src/createSelectorCreator.ts @@ -420,7 +420,6 @@ export function createSelectorCreator< if ( process.env.NODE_ENV !== 'production' && - inputSelectorResults.length && shouldRunInputStabilityCheck(noopCheck, firstRun) ) { runNoopCheck(resultFunc as Combiner) diff --git a/test/autotrackMemoize.spec.ts b/test/autotrackMemoize.spec.ts index c21bc301f..985ce51b2 100644 --- a/test/autotrackMemoize.spec.ts +++ b/test/autotrackMemoize.spec.ts @@ -1,7 +1,8 @@ import { - createSelectorCreator, - unstable_autotrackMemoize as autotrackMemoize + unstable_autotrackMemoize as autotrackMemoize, + createSelectorCreator } from 'reselect' +import { setEnvToProd } 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 @@ -97,30 +98,23 @@ describe('Basic selector behavior with autotrack', () => { ) }) - describe('performance checks', () => { - const originalEnv = process.env.NODE_ENV + const isCoverage = process.env.COVERAGE - beforeAll(() => { - process.env.NODE_ENV = 'production' - }) - afterAll(() => { - process.env.NODE_NV = originalEnv - }) + // don't run performance tests for coverage + describe.skipIf(isCoverage)('performance checks', () => { + beforeAll(setEnvToProd) test('basic selector cache hit performance', () => { - if (process.env.COVERAGE) { - return // don't run performance tests for coverage - } - const selector = createSelector( (state: StateAB) => state.a, (state: StateAB) => state.b, - (a, b) => a + b + (a, b) => a + b, + { noopCheck: 'never' } ) const state1 = { a: 1, b: 2 } const start = performance.now() - for (let i = 0; i < 1000000; i++) { + for (let i = 0; i < 1_000_000; i++) { selector(state1) } const totalTime = performance.now() - start @@ -132,18 +126,15 @@ describe('Basic selector behavior with autotrack', () => { }) test('basic selector cache hit performance for state changes but shallowly equal selector args', () => { - if (process.env.COVERAGE) { - return // don't run performance tests for coverage - } - const selector = createSelector( (state: StateAB) => state.a, (state: StateAB) => state.b, - (a, b) => a + b + (a, b) => a + b, + { noopCheck: 'never' } ) const start = performance.now() - for (let i = 0; i < 1000000; i++) { + for (let i = 0; i < 1_000_000; i++) { selector(states[i]) } const totalTime = performance.now() - start @@ -205,7 +196,8 @@ describe('Basic selector behavior with autotrack', () => { () => { called++ throw Error('test error') - } + }, + { noopCheck: 'never' } ) expect(() => selector({ a: 1 })).toThrow('test error') expect(() => selector({ a: 1 })).toThrow('test error') @@ -220,7 +212,8 @@ describe('Basic selector behavior with autotrack', () => { called++ if (a > 1) throw Error('test error') return a - } + }, + { noopCheck: 'never' } ) const state1 = { a: 1 } const state2 = { a: 2 } diff --git a/test/examples.test.ts b/test/examples.test.ts index f487097e7..8f6218873 100644 --- a/test/examples.test.ts +++ b/test/examples.test.ts @@ -1,220 +1,220 @@ -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), - { inputStabilityCheck: 'never' } - ) - - nonMemoizedSelector(store.getState()) - nonMemoizedSelector(store.getState()) - nonMemoizedSelector(store.getState()) - - expect(nonMemoizedSelector.recomputations()).toBe(3) -}) - -test.todo('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.todo('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 - } -}) - -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 - ) -}) - -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) - ) -}) +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), + { inputStabilityCheck: 'never' } + ) + + nonMemoizedSelector(store.getState()) + nonMemoizedSelector(store.getState()) + nonMemoizedSelector(store.getState()) + + expect(nonMemoizedSelector.recomputations()).toBe(3) +}) + +test.todo('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.todo('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 + } +}) + +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 + ) +}) + +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) + ) +}) diff --git a/test/reselect.spec.ts b/test/reselect.spec.ts index d7504e726..0e90023a8 100644 --- a/test/reselect.spec.ts +++ b/test/reselect.spec.ts @@ -12,7 +12,13 @@ import { import type { OutputSelector, OutputSelectorFields } from 'reselect' import type { RootState } from './testUtils' -import { addTodo, deepClone, localTest, toggleCompleted } from './testUtils' +import { + addTodo, + deepClone, + localTest, + setEnvToProd, + 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 @@ -105,21 +111,13 @@ describe('Basic selector behavior', () => { ) }) - describe('performance checks', () => { - const originalEnv = process.env.NODE_ENV - - beforeAll(() => { - process.env.NODE_ENV = 'production' - }) - afterAll(() => { - process.env.NODE_ENV = originalEnv - }) + const isCoverage = process.env.COVERAGE - test('basic selector cache hit performance', () => { - if (process.env.COVERAGE) { - return // don't run performance tests for coverage - } + describe('performance checks', () => { + beforeAll(setEnvToProd) + // don't run performance tests for coverage + test.skipIf(isCoverage)('basic selector cache hit performance', () => { const selector = createSelector( (state: StateAB) => state.a, (state: StateAB) => state.b, @@ -128,7 +126,7 @@ describe('Basic selector behavior', () => { const state1 = { a: 1, b: 2 } const start = performance.now() - for (let i = 0; i < 1000000; i++) { + for (let i = 0; i < 1_000_000; i++) { selector(state1) } const totalTime = performance.now() - start @@ -139,25 +137,24 @@ describe('Basic selector behavior', () => { expect(totalTime).toBeLessThan(2000) }) - test('basic selector cache hit performance for state changes but shallowly equal selector args', () => { - if (process.env.COVERAGE) { - return // don't run performance tests for coverage - } - - const selector = createSelector( - (state: StateAB) => state.a, - (state: StateAB) => state.b, - (a, b) => a + b - ) + // don't run performance tests for coverage + test.skipIf(isCoverage)( + 'basic selector cache hit performance for state changes but shallowly equal selector args', + () => { + const selector = createSelector( + (state: StateAB) => state.a, + (state: StateAB) => state.b, + (a, b) => a + b + ) - const start = new Date() - for (let i = 0; i < numOfStates; i++) { - selector(states[i]) - } - const totalTime = new Date().getTime() - start.getTime() + const start = new Date() + for (let i = 0; i < numOfStates; i++) { + selector(states[i]) + } + const totalTime = new Date().getTime() - start.getTime() - expect(selector(states[0])).toBe(3) - expect(selector.recomputations()).toBe(1) + expect(selector(states[0])).toBe(3) + expect(selector.recomputations()).toBe(1) // Expected a million calls to a selector with the same arguments to take less than 1 second expect(totalTime).toBeLessThan(2000) diff --git a/test/testUtils.ts b/test/testUtils.ts index 2de37995d..530484f44 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -566,3 +566,11 @@ export const expensiveComputation = (times = 1_000_000) => { // Do nothing } } + +export const setEnvToProd = () => { + const originalEnv = process.env.NODE_ENV + process.env.NODE_ENV = 'production' + return () => { + process.env.NODE_ENV = originalEnv + } +} diff --git a/test/weakmapMemoize.spec.ts b/test/weakmapMemoize.spec.ts index fb66ff23f..7252c306b 100644 --- a/test/weakmapMemoize.spec.ts +++ b/test/weakmapMemoize.spec.ts @@ -1,4 +1,5 @@ -import { createSelectorCreator, weakMapMemoize } from 'reselect' +import { createSelector, createSelectorCreator, weakMapMemoize } from 'reselect' +import { setEnvToProd } 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 @@ -94,54 +95,6 @@ describe('Basic selector behavior with weakMapMemoize', () => { ) }) - test('basic selector cache hit performance', () => { - if (process.env.COVERAGE) { - return // don't run performance tests for coverage - } - - const selector = createSelector( - (state: StateAB) => state.a, - (state: StateAB) => state.b, - (a, b) => a + b - ) - const state1 = { a: 1, b: 2 } - - const start = performance.now() - for (let i = 0; i < 1000000; i++) { - selector(state1) - } - const totalTime = performance.now() - start - - expect(selector(state1)).toBe(3) - expect(selector.recomputations()).toBe(1) - // Expected a million calls to a selector with the same arguments to take less than 2 seconds - expect(totalTime).toBeLessThan(200) - }) - - test('basic selector cache hit performance for state changes but shallowly equal selector args', () => { - if (process.env.COVERAGE) { - return // don't run performance tests for coverage - } - - const selector = createSelector( - (state: StateAB) => state.a, - (state: StateAB) => state.b, - (a, b) => a + b - ) - - const start = performance.now() - for (let i = 0; i < 1000000; i++) { - selector(states[i]) - } - const totalTime = performance.now() - start - - expect(selector(states[0])).toBe(3) - expect(selector.recomputations()).toBe(1) - - // Expected a million calls to a selector with the same arguments to take less than 1 second - expect(totalTime).toBeLessThan(2000) - }) - test('memoized composite arguments', () => { const selector = createSelector( (state: StateSub) => state.sub, @@ -217,3 +170,50 @@ describe('Basic selector behavior with weakMapMemoize', () => { expect(called).toBe(2) }) }) + +const isCoverage = process.env.COVERAGE + +// don't run performance tests for coverage +describe.skipIf(isCoverage)('weakmapMemoize performance tests', () => { + beforeAll(setEnvToProd) + + test('basic selector cache hit performance', () => { + const selector = createSelector( + (state: StateAB) => state.a, + (state: StateAB) => state.b, + (a, b) => a + b + ) + const state1 = { a: 1, b: 2 } + + const start = performance.now() + for (let i = 0; i < 1_000_000; i++) { + selector(state1) + } + const totalTime = performance.now() - start + + expect(selector(state1)).toBe(3) + expect(selector.recomputations()).toBe(1) + // Expected a million calls to a selector with the same arguments to take less than 1 second + expect(totalTime).toBeLessThan(2000) + }) + + test('basic selector cache hit performance for state changes but shallowly equal selector args', () => { + const selector = createSelector( + (state: StateAB) => state.a, + (state: StateAB) => state.b, + (a, b) => a + b + ) + + const start = performance.now() + for (let i = 0; i < 1_000_000; i++) { + selector(states[i]) + } + const totalTime = performance.now() - start + + expect(selector(states[0])).toBe(3) + expect(selector.recomputations()).toBe(1) + + // Expected a million calls to a selector with the same arguments to take less than 1 second + expect(totalTime).toBeLessThan(2000) + }) +}) From 1615733dbc2573046ec9efd3069526f9023ba9e1 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sun, 26 Nov 2023 05:28:02 -0600 Subject: [PATCH 04/24] Rename `StabilityCheckFrequency` to `DevModeCheckFrequency` --- README.md | 2 +- src/createSelectorCreator.ts | 50 +++++++++++++++++------------------- src/index.ts | 2 +- src/types.ts | 6 ++--- src/utils.ts | 8 +++--- test/weakmapMemoize.spec.ts | 6 +++-- 6 files changed, 36 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 586926be0..161bb371d 100644 --- a/README.md +++ b/README.md @@ -1079,7 +1079,7 @@ 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' +type DevModeCheckFrequency = 'always' | 'once' | 'never' ``` | Possible Values | Description | diff --git a/src/createSelectorCreator.ts b/src/createSelectorCreator.ts index ee10fe798..45668c084 100644 --- a/src/createSelectorCreator.ts +++ b/src/createSelectorCreator.ts @@ -3,6 +3,7 @@ import { weakMapMemoize } from './weakMapMemoize' import type { Combiner, CreateSelectorOptions, + DevModeCheckFrequency, DropFirstParameter, ExtractMemoizerFields, GetParamsFromSelectors, @@ -13,7 +14,6 @@ import type { SelectorArray, SetRequired, Simplify, - StabilityCheckFrequency, UnknownMemoizer } from './types' @@ -24,7 +24,7 @@ import { getDependencies, runNoopCheck, runStabilityCheck, - shouldRunInputStabilityCheck + shouldRunDevModeCheck } from './utils' /** @@ -144,7 +144,7 @@ export interface CreateSelectorFunction< InterruptRecursion } -let globalStabilityCheck: StabilityCheckFrequency = 'once' +let globalStabilityCheck: DevModeCheckFrequency = 'once' /** * In development mode, an extra check is conducted on your input selectors. @@ -183,15 +183,15 @@ import { OutputSelectorFields, Mapped } from './types'; * @public */ export function setInputStabilityCheckEnabled( - inputStabilityCheckFrequency: StabilityCheckFrequency + inputStabilityCheckFrequency: DevModeCheckFrequency ) { globalStabilityCheck = inputStabilityCheckFrequency } -let globalNoopCheck: StabilityCheckFrequency = 'once' +let globalNoopCheck: DevModeCheckFrequency = 'once' export const setGlobalNoopCheck = ( - noopCheckFrequency: StabilityCheckFrequency + noopCheckFrequency: DevModeCheckFrequency ) => { globalNoopCheck = noopCheckFrequency } @@ -418,30 +418,26 @@ export function createSelectorCreator< arguments ) - if ( - process.env.NODE_ENV !== 'production' && - shouldRunInputStabilityCheck(noopCheck, firstRun) - ) { - runNoopCheck(resultFunc as Combiner) - } + if (process.env.NODE_ENV !== 'production') { + if (shouldRunDevModeCheck(noopCheck, firstRun)) { + runNoopCheck(resultFunc as Combiner) + } - if ( - process.env.NODE_ENV !== 'production' && - shouldRunInputStabilityCheck(inputStabilityCheck, firstRun) - ) { - // make a second copy of the params, to check if we got the same results - const inputSelectorResultsCopy = collectInputSelectorResults( - dependencies, - arguments - ) + if (shouldRunDevModeCheck(inputStabilityCheck, firstRun)) { + // make a second copy of the params, to check if we got the same results + const inputSelectorResultsCopy = collectInputSelectorResults( + dependencies, + arguments + ) - runStabilityCheck( - { inputSelectorResults, inputSelectorResultsCopy }, - { memoize, memoizeOptions: finalMemoizeOptions }, - arguments - ) + runStabilityCheck( + { inputSelectorResults, inputSelectorResultsCopy }, + { memoize, memoizeOptions: finalMemoizeOptions }, + arguments + ) - if (firstRun) firstRun = false + if (firstRun) firstRun = false + } } // apply arguments instead of spreading for performance. diff --git a/src/index.ts b/src/index.ts index cf52fc80d..d924291a0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,7 @@ export type { Combiner, CreateSelectorOptions, DefaultMemoizeFields, + DevModeCheckFrequency, EqualityFn, ExtractMemoizerFields, GetParamsFromSelectors, @@ -28,7 +29,6 @@ export type { Selector, SelectorArray, SelectorResultArray, - StabilityCheckFrequency, UnknownMemoizer } from './types' export { weakMapMemoize } from './weakMapMemoize' diff --git a/src/types.ts b/src/types.ts index ba3bfc878..b1342fa7d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -81,9 +81,9 @@ export interface CreateSelectorOptions< * * @since 5.0.0 */ - inputStabilityCheck?: StabilityCheckFrequency + inputStabilityCheck?: DevModeCheckFrequency - noopCheck?: StabilityCheckFrequency + noopCheck?: DevModeCheckFrequency /** * The memoize function that is used to memoize the {@linkcode OutputSelectorFields.resultFunc resultFunc} @@ -305,7 +305,7 @@ export type EqualityFn = (a: T, b: T) => boolean * @since 5.0.0 * @public */ -export type StabilityCheckFrequency = 'always' | 'once' | 'never' +export type DevModeCheckFrequency = 'always' | 'once' | 'never' /** * Determines the combined single "State" type (first arg) from all input selectors. diff --git a/src/utils.ts b/src/utils.ts index 42b60a0db..4161253ad 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,9 +1,9 @@ import type { AnyFunction, CreateSelectorOptions, + DevModeCheckFrequency, Selector, SelectorArray, - StabilityCheckFrequency, UnknownMemoizer } from './types' @@ -168,7 +168,7 @@ export function runStabilityCheck( } } -export const runNoopCheck = (resultFunc: Func) => { +export const runNoopCheck = (resultFunc: AnyFunction) => { let isInputSameAsOutput = false try { const emptyObject = {} @@ -193,8 +193,8 @@ export const runNoopCheck = (resultFunc: Func) => { * @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, +export const shouldRunDevModeCheck = ( + inputStabilityCheck: DevModeCheckFrequency, firstRun: boolean ) => { return ( diff --git a/test/weakmapMemoize.spec.ts b/test/weakmapMemoize.spec.ts index 7252c306b..784f7d8be 100644 --- a/test/weakmapMemoize.spec.ts +++ b/test/weakmapMemoize.spec.ts @@ -181,7 +181,8 @@ describe.skipIf(isCoverage)('weakmapMemoize performance tests', () => { const selector = createSelector( (state: StateAB) => state.a, (state: StateAB) => state.b, - (a, b) => a + b + (a, b) => a + b, + { noopCheck: 'never' } ) const state1 = { a: 1, b: 2 } @@ -201,7 +202,8 @@ describe.skipIf(isCoverage)('weakmapMemoize performance tests', () => { const selector = createSelector( (state: StateAB) => state.a, (state: StateAB) => state.b, - (a, b) => a + b + (a, b) => a + b, + { noopCheck: 'never' } ) const start = performance.now() From cdc93c06d63900f62cc8fba618d5b5861e50136b Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sun, 26 Nov 2023 05:56:24 -0600 Subject: [PATCH 05/24] Rename `noopCheck` to `identityFunctionCheck` - Rename the types, functions and variables to identity function check, since noop is more like `() => {}` and identity is `x => x` --- src/createSelectorCreator.ts | 18 ++++++++++-------- src/index.ts | 2 +- src/types.ts | 2 +- src/utils.ts | 2 +- test/autotrackMemoize.spec.ts | 12 ++++++------ test/defaultMemoize.spec.ts | 2 +- ...k.test.ts => identityFunctionCheck.test.ts} | 17 +++++------------ test/reselect.spec.ts | 14 +++++++------- test/selectorUtils.spec.ts | 2 +- test/weakmapMemoize.spec.ts | 10 +++++----- 10 files changed, 38 insertions(+), 43 deletions(-) rename test/{noopCheck.test.ts => identityFunctionCheck.test.ts} (88%) diff --git a/src/createSelectorCreator.ts b/src/createSelectorCreator.ts index 45668c084..54f39f616 100644 --- a/src/createSelectorCreator.ts +++ b/src/createSelectorCreator.ts @@ -22,7 +22,7 @@ import { collectInputSelectorResults, ensureIsArray, getDependencies, - runNoopCheck, + runIdentityFunctionCheck, runStabilityCheck, shouldRunDevModeCheck } from './utils' @@ -188,12 +188,12 @@ export function setInputStabilityCheckEnabled( globalStabilityCheck = inputStabilityCheckFrequency } -let globalNoopCheck: DevModeCheckFrequency = 'once' +let globalIdentityFunctionCheck: DevModeCheckFrequency = 'once' -export const setGlobalNoopCheck = ( - noopCheckFrequency: DevModeCheckFrequency +export const setGlobalIdentityFunctionCheck = ( + identityFunctionCheckFrequency: DevModeCheckFrequency ) => { - globalNoopCheck = noopCheckFrequency + globalIdentityFunctionCheck = identityFunctionCheckFrequency } /** @@ -384,7 +384,7 @@ export function createSelectorCreator< argsMemoize = weakMapMemoize, argsMemoizeOptions = [], inputStabilityCheck = globalStabilityCheck, - noopCheck = globalNoopCheck + identityFunctionCheck = globalIdentityFunctionCheck } = combinedOptions // Simplifying assumption: it's unlikely that the first options arg of the provided memoizer @@ -419,8 +419,10 @@ export function createSelectorCreator< ) if (process.env.NODE_ENV !== 'production') { - if (shouldRunDevModeCheck(noopCheck, firstRun)) { - runNoopCheck(resultFunc as Combiner) + if (shouldRunDevModeCheck(identityFunctionCheck, firstRun)) { + runIdentityFunctionCheck( + resultFunc as Combiner + ) } if (shouldRunDevModeCheck(inputStabilityCheck, firstRun)) { diff --git a/src/index.ts b/src/index.ts index d924291a0..d73c33f18 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ export { autotrackMemoize as unstable_autotrackMemoize } from './autotrackMemoiz export { createSelector, createSelectorCreator, - setGlobalNoopCheck, + setGlobalIdentityFunctionCheck, setInputStabilityCheckEnabled } from './createSelectorCreator' export type { CreateSelectorFunction } from './createSelectorCreator' diff --git a/src/types.ts b/src/types.ts index b1342fa7d..7fd6a291e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -83,7 +83,7 @@ export interface CreateSelectorOptions< */ inputStabilityCheck?: DevModeCheckFrequency - noopCheck?: DevModeCheckFrequency + identityFunctionCheck?: DevModeCheckFrequency /** * The memoize function that is used to memoize the {@linkcode OutputSelectorFields.resultFunc resultFunc} diff --git a/src/utils.ts b/src/utils.ts index 4161253ad..00903a71e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -168,7 +168,7 @@ export function runStabilityCheck( } } -export const runNoopCheck = (resultFunc: AnyFunction) => { +export const runIdentityFunctionCheck = (resultFunc: AnyFunction) => { let isInputSameAsOutput = false try { const emptyObject = {} diff --git a/test/autotrackMemoize.spec.ts b/test/autotrackMemoize.spec.ts index 985ce51b2..666be5618 100644 --- a/test/autotrackMemoize.spec.ts +++ b/test/autotrackMemoize.spec.ts @@ -35,7 +35,7 @@ describe('Basic selector behavior with autotrack', () => { const selector = createSelector( (state: StateA) => state.a, a => a, - { noopCheck: 'never' } + { identityFunctionCheck: 'never' } ) const firstState = { a: 1 } const firstStateNewPointer = { a: 1 } @@ -54,7 +54,7 @@ describe('Basic selector behavior with autotrack', () => { const selector = createSelector( (...params: any[]) => params.length, a => a, - { noopCheck: 'never' } + { identityFunctionCheck: 'never' } ) expect(selector({})).toBe(1) }) @@ -109,7 +109,7 @@ describe('Basic selector behavior with autotrack', () => { (state: StateAB) => state.a, (state: StateAB) => state.b, (a, b) => a + b, - { noopCheck: 'never' } + { identityFunctionCheck: 'never' } ) const state1 = { a: 1, b: 2 } @@ -130,7 +130,7 @@ describe('Basic selector behavior with autotrack', () => { (state: StateAB) => state.a, (state: StateAB) => state.b, (a, b) => a + b, - { noopCheck: 'never' } + { identityFunctionCheck: 'never' } ) const start = performance.now() @@ -197,7 +197,7 @@ describe('Basic selector behavior with autotrack', () => { called++ throw Error('test error') }, - { noopCheck: 'never' } + { identityFunctionCheck: 'never' } ) expect(() => selector({ a: 1 })).toThrow('test error') expect(() => selector({ a: 1 })).toThrow('test error') @@ -213,7 +213,7 @@ describe('Basic selector behavior with autotrack', () => { if (a > 1) throw Error('test error') return a }, - { noopCheck: 'never' } + { identityFunctionCheck: 'never' } ) const state1 = { a: 1 } const state2 = { a: 2 } diff --git a/test/defaultMemoize.spec.ts b/test/defaultMemoize.spec.ts index 716bec6c1..f3f754bcf 100644 --- a/test/defaultMemoize.spec.ts +++ b/test/defaultMemoize.spec.ts @@ -369,7 +369,7 @@ describe('defaultMemoize', () => { }, { memoizeOptions: { maxSize: 3 }, - noopCheck: 'never' + identityFunctionCheck: 'never' } ) diff --git a/test/noopCheck.test.ts b/test/identityFunctionCheck.test.ts similarity index 88% rename from test/noopCheck.test.ts rename to test/identityFunctionCheck.test.ts index f5771ba4d..07f62c0b6 100644 --- a/test/noopCheck.test.ts +++ b/test/identityFunctionCheck.test.ts @@ -1,4 +1,4 @@ -import { createSelector, setGlobalNoopCheck } from 'reselect' +import { createSelector, setGlobalIdentityFunctionCheck } from 'reselect' import type { LocalTestContext, RootState } from './testUtils' import { localTest } from './testUtils' @@ -40,7 +40,7 @@ describe('noopCheck', () => { ) localTest('disables check if global setting is set to never', ({ state }) => { - setGlobalNoopCheck('never') + setGlobalIdentityFunctionCheck('never') expect(badSelector(state)).toBe(state) @@ -48,7 +48,7 @@ describe('noopCheck', () => { expect(consoleSpy).not.toHaveBeenCalled() - setGlobalNoopCheck('once') + setGlobalIdentityFunctionCheck('once') }) localTest( @@ -57,7 +57,7 @@ describe('noopCheck', () => { const badSelector = createSelector( [(state: RootState) => state], identityFunction, - { noopCheck: 'never' } + { identityFunctionCheck: 'never' } ) expect(badSelector(state)).toBe(state) @@ -85,7 +85,7 @@ describe('noopCheck', () => { const badSelector = createSelector( [(state: RootState) => state], identityFunction, - { noopCheck: 'once' } + { identityFunctionCheck: 'once' } ) expect(badSelector(state)).toBe(state) @@ -103,9 +103,6 @@ describe('noopCheck', () => { }) localTest('uses the memoize provided', ({ state }) => { - // console.log(setupStore) - // const store = setupStore() - // const state = store.getState() const badSelector = createSelector( [(state: RootState) => state.todos], identityFunction @@ -116,12 +113,8 @@ describe('noopCheck', () => { expect(consoleSpy).toHaveBeenCalledTimes(1) - const newState = { ...state } - expect(badSelector({ ...state })).not.toBe(state) - // expect(identityFunction).toHaveBeenCalledTimes(3) - expect(consoleSpy).toHaveBeenCalledTimes(1) }) }) diff --git a/test/reselect.spec.ts b/test/reselect.spec.ts index 0e90023a8..8d633034c 100644 --- a/test/reselect.spec.ts +++ b/test/reselect.spec.ts @@ -3,10 +3,10 @@ import lodashMemoize from 'lodash/memoize' import microMemoize from 'micro-memoize' import { + unstable_autotrackMemoize as autotrackMemoize, createSelector, createSelectorCreator, defaultMemoize, - unstable_autotrackMemoize as autotrackMemoize, weakMapMemoize } from 'reselect' @@ -48,7 +48,7 @@ describe('Basic selector behavior', () => { const selector = createSelector( (state: StateA) => state.a, a => a, - { noopCheck: 'never' } + { identityFunctionCheck: 'never' } ) const firstState = { a: 1 } const firstStateNewPointer = { a: 1 } @@ -67,7 +67,7 @@ describe('Basic selector behavior', () => { const selector = createSelector( (...params: any[]) => params.length, a => a, - { noopCheck: 'never' } + { identityFunctionCheck: 'never' } ) expect(selector({})).toBe(1) }) @@ -164,7 +164,7 @@ describe('Basic selector behavior', () => { const selector = createSelector( (state: StateSub) => state.sub, sub => sub, - { noopCheck: 'never' } + { identityFunctionCheck: 'never' } ) const state1 = { sub: { a: 1 } } expect(selector(state1)).toEqual({ a: 1 }) @@ -226,7 +226,7 @@ describe('Basic selector behavior', () => { if (a > 1) throw Error('test error') return a }, - { noopCheck: 'never' } + { identityFunctionCheck: 'never' } ) const state1 = { a: 1 } const state2 = { a: 2 } @@ -242,7 +242,7 @@ describe('Combining selectors', () => { const selector1 = createSelector( (state: StateSub) => state.sub, sub => sub, - { noopCheck: 'never' } + { identityFunctionCheck: 'never' } ) const selector2 = createSelector(selector1, sub => sub.a) const state1 = { sub: { a: 1 } } @@ -304,7 +304,7 @@ describe('Combining selectors', () => { const selector = createOverridenSelector( (state: StateA) => state.a, a => a, - { noopCheck: 'never' } + { identityFunctionCheck: 'never' } ) expect(selector({ a: 1 })).toBe(1) expect(selector({ a: 2 })).toBe(1) // yes, really true diff --git a/test/selectorUtils.spec.ts b/test/selectorUtils.spec.ts index 526bea33f..655891db1 100644 --- a/test/selectorUtils.spec.ts +++ b/test/selectorUtils.spec.ts @@ -9,7 +9,7 @@ describe('createSelector exposed utils', () => { { memoize: defaultMemoize, argsMemoize: defaultMemoize, - noopCheck: 'never' + identityFunctionCheck: 'never' } ) expect(selector({ a: 1 })).toBe(1) diff --git a/test/weakmapMemoize.spec.ts b/test/weakmapMemoize.spec.ts index 784f7d8be..f16c23839 100644 --- a/test/weakmapMemoize.spec.ts +++ b/test/weakmapMemoize.spec.ts @@ -32,7 +32,7 @@ describe('Basic selector behavior with weakMapMemoize', () => { const selector = createSelector( (state: StateA) => state.a, a => a, - { noopCheck: 'never' } + { identityFunctionCheck: 'never' } ) const firstState = { a: 1 } const firstStateNewPointer = { a: 1 } @@ -51,7 +51,7 @@ describe('Basic selector behavior with weakMapMemoize', () => { const selector = createSelector( (...params: any[]) => params.length, a => a, - { noopCheck: 'never' } + { identityFunctionCheck: 'never' } ) expect(selector({})).toBe(1) }) @@ -160,7 +160,7 @@ describe('Basic selector behavior with weakMapMemoize', () => { if (a > 1) throw Error('test error') return a }, - { noopCheck: 'never' } + { identityFunctionCheck: 'never' } ) const state1 = { a: 1 } const state2 = { a: 2 } @@ -182,7 +182,7 @@ describe.skipIf(isCoverage)('weakmapMemoize performance tests', () => { (state: StateAB) => state.a, (state: StateAB) => state.b, (a, b) => a + b, - { noopCheck: 'never' } + { identityFunctionCheck: 'never' } ) const state1 = { a: 1, b: 2 } @@ -203,7 +203,7 @@ describe.skipIf(isCoverage)('weakmapMemoize performance tests', () => { (state: StateAB) => state.a, (state: StateAB) => state.b, (a, b) => a + b, - { noopCheck: 'never' } + { identityFunctionCheck: 'never' } ) const start = performance.now() From b25c9c98b901d7ecb95c877d951b13fd92167a84 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sun, 26 Nov 2023 06:19:29 -0600 Subject: [PATCH 06/24] Add JSDocs for `runIdentityFunctionCheck` utility function. --- src/utils.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index 00903a71e..7e9b80cdd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -123,7 +123,7 @@ export function collectInputSelectorResults( } /** - * Run a stability check to ensure the input selector results remain stable + * Runs a stability check to ensure the input selector results remain stable * when provided with the same arguments. This function is designed to detect * changes in the output of input selectors, which can impact the performance of memoized selectors. * @@ -168,6 +168,16 @@ export function runStabilityCheck( } } +/** + * Runs a check to determine if the given result function behaves as an + * identity function. An identity function is one that returns its + * input unchanged, for example, `x => x`. This check helps ensure + * efficient memoization and prevent unnecessary re-renders by encouraging + * proper use of transformation logic in result functions and + * extraction logic in input selectors. + * + * @param resultFunc - The result function to be checked. + */ export const runIdentityFunctionCheck = (resultFunc: AnyFunction) => { let isInputSameAsOutput = false try { From ac4da4a0601c276aa38e6b02452cf864b5bbb8f6 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sun, 26 Nov 2023 07:29:53 -0600 Subject: [PATCH 07/24] Update `README` to include `identityFunctionCheck` --- README.md | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 161bb371d..47134ea98 100644 --- a/README.md +++ b/README.md @@ -1115,10 +1115,10 @@ setInputStabilityCheckEnabled('never') ##### 2. Per selector by passing an `inputStabilityCheck` option directly to [`createSelector`]: ```ts -// Create a selector that double-checks the results of [`input selectors`][Input Selectors] every time it runs. +// 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. + // This input selector will not be memoized properly since it always returns a new reference. (state: RootState) => state.todos.filter(({ completed }) => completed === true) ], @@ -1131,6 +1131,75 @@ const selectCompletedTodosLength = createSelector( > [!WARNING] > This will override the global input stability check set by calling `setInputStabilityCheckEnabled`. + + +#### `identityFunctionCheck` + +When working with Reselect, it's crucial to adhere to a fundamental philosophy regarding the separation of concerns between extraction and transformation logic. + +- **Extraction Logic**: This refers to operations like `state => state.todos`, which should be placed in [input selectors]. Extraction logic is responsible for retrieving or 'selecting' data from a broader state or dataset. + +- **Transformation Logic**: In contrast, transformation logic, such as `todos => todos.map(({ id }) => id)`, belongs in the [result function]. This is where you manipulate, format, or transform the data extracted by the input selectors. + +Most importantly, effective memoization in Reselect hinges on following these guidelines. Memoization, only functions correctly when extraction and transformation logic are properly segregated. By keeping extraction logic in input selectors and transformation logic in the result function, Reselect can efficiently determine when to reuse cached results and when to recompute them. This not only enhances performance but also ensures the consistency and predictability of your selectors. + +For memoization to work as intended, it's imperative to follow both guidelines. If either is disregarded, memoization will not function properly. Consider the following example for clarity: + +```ts +// ❌ Incorrect Use Case: This will not memoize correctly, and does nothing useful! +const brokenSelector = createSelector( + // ✔️ GOOD: Contains extraction logic. + [(state: RootState) => state.todos], + // ❌ BAD: Does not contain transformation logic. + todos => todos +) +``` + +```ts +type DevModeCheckFrequency = '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 identity function check. | + +> [!IMPORTANT] +> The identity function check is automatically disabled in production environments. + +You can configure this behavior in two ways: + + + +##### 1. Globally through `setGlobalIdentityFunctionCheck`: + +```ts +import { setGlobalIdentityFunctionCheck } from 'reselect' + +// Run only the first time the selector is called. (default) +setGlobalIdentityFunctionCheck('once') + +// Run every time the selector is called. +setGlobalIdentityFunctionCheck('always') + +// Never run the identity function check. +setGlobalIdentityFunctionCheck('never') +``` + +##### 2. Per selector by passing an `identityFunctionCheck` option directly to [`createSelector`]: + +```ts +// Create a selector that checks to see if the result function is an identity function. +const selectTodos = createSelector( + [(state: RootState) => state.todos], + // This result function does not contain any transformation logic. + todos => todos, + // Will override the global setting. + { inputStabilityCheck: 'always' } +) +``` + ### Output Selector Fields From 5a53b4274f0bab04575b132ea41a7b39fe386b1c Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sun, 26 Nov 2023 07:49:03 -0600 Subject: [PATCH 08/24] Add JSDocs for `setGlobalIdentityFunctionCheck` --- src/createSelectorCreator.ts | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/createSelectorCreator.ts b/src/createSelectorCreator.ts index 54f39f616..22f33f514 100644 --- a/src/createSelectorCreator.ts +++ b/src/createSelectorCreator.ts @@ -164,8 +164,6 @@ let globalStabilityCheck: DevModeCheckFrequency = 'once' * @example * ```ts * import { setInputStabilityCheckEnabled } from 'reselect' -import { assert } from './autotrackMemoize/utils'; -import { OutputSelectorFields, Mapped } from './types'; * * // Run only the first time the selector is called. (default) * setInputStabilityCheckEnabled('once') @@ -190,6 +188,40 @@ export function setInputStabilityCheckEnabled( let globalIdentityFunctionCheck: DevModeCheckFrequency = 'once' +/** + * In development mode, an extra check is conducted on your result function. + * It runs your result function an extra time, and + * warns in the console if it returns its own input. + * + * 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#2-per-selector-by-passing-an-identityfunctioncheck-option-directly-to-createselector per-selector-configuration} + * and {@linkcode CreateSelectorOptions.identityFunctionCheck identityFunctionCheck} for more details. + * + * _The identity function check does not run in production builds._ + * + * @param identityFunctionCheckFrequency - How often the `identityFunctionCheck` should run for all selectors. + * + * @example + * ```ts + * import { setGlobalIdentityFunctionCheck } from 'reselect' + * + * // Run only the first time the selector is called. (default) + * setGlobalIdentityFunctionCheck('once') + * + * // Run every time the selector is called. + * setGlobalIdentityFunctionCheck('always') + * + * // Never run the identity function check. + * setGlobalIdentityFunctionCheck('never') + * ``` + * @see {@link https://github.com/reduxjs/reselect#debugging-tools debugging-tools} + * @see {@link https://github.com/reduxjs/reselect#1-globally-through-setGlobalIdentityFunctionCheck global-configuration} + * + * @since 5.0.0 + * @public + */ export const setGlobalIdentityFunctionCheck = ( identityFunctionCheckFrequency: DevModeCheckFrequency ) => { From 751daf37922fc91f75ff4a702a20871d128c1958 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sun, 26 Nov 2023 07:59:00 -0600 Subject: [PATCH 09/24] Add JSDocs for `CreateSelectorOptions.identityFunctionCheck` --- src/createSelectorCreator.ts | 2 +- src/types.ts | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/createSelectorCreator.ts b/src/createSelectorCreator.ts index 22f33f514..5122e614e 100644 --- a/src/createSelectorCreator.ts +++ b/src/createSelectorCreator.ts @@ -217,7 +217,7 @@ let globalIdentityFunctionCheck: DevModeCheckFrequency = 'once' * setGlobalIdentityFunctionCheck('never') * ``` * @see {@link https://github.com/reduxjs/reselect#debugging-tools debugging-tools} - * @see {@link https://github.com/reduxjs/reselect#1-globally-through-setGlobalIdentityFunctionCheck global-configuration} + * @see {@link https://github.com/reduxjs/reselect#1-globally-through-setglobalidentityfunctioncheck global-configuration} * * @since 5.0.0 * @public diff --git a/src/types.ts b/src/types.ts index 7fd6a291e..fbb797dec 100644 --- a/src/types.ts +++ b/src/types.ts @@ -83,6 +83,20 @@ export interface CreateSelectorOptions< */ inputStabilityCheck?: DevModeCheckFrequency + /** + * Overrides the global identity function check for the selector. + * - `once` - Run only the first time the selector is called. + * - `always` - Run every time the selector is called. + * - `never` - Never run the identity function check. + * + * @default 'once' + * + * @see {@link https://github.com/reduxjs/reselect#debugging-tools debugging-tools} + * @see {@link https://github.com/reduxjs/reselect#identityfunctioncheck identityFunctionCheck} + * @see {@link https://github.com/reduxjs/reselect#2-per-selector-by-passing-an-identityfunctioncheck-option-directly-to-createselector per-selector-configuration} + * + * @since 5.0.0 + */ identityFunctionCheck?: DevModeCheckFrequency /** From 611d83349280278b59c05ab73d80d7fe9ba84af1 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sun, 26 Nov 2023 08:02:55 -0600 Subject: [PATCH 10/24] Add `identityFunctionCheck` to v5 summary in `README` --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 47134ea98..1a85deea0 100644 --- a/README.md +++ b/README.md @@ -1253,6 +1253,7 @@ Version 5.0.0 introduces several new features and improvements: - 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. + - Introduced `identityFunctionCheck`, a development tool that checks to see if the [result function] returns its own input. These updates aim to enhance flexibility, performance, and developer experience. For detailed usage and examples, refer to the updated documentation sections for each feature. From c2563d364a62d2a79d5cd94c7ad52fbebe610679 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sun, 26 Nov 2023 09:18:27 -0600 Subject: [PATCH 11/24] Fix copy-paste error in `README` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1a85deea0..17f7ebd0c 100644 --- a/README.md +++ b/README.md @@ -1196,7 +1196,7 @@ const selectTodos = createSelector( // This result function does not contain any transformation logic. todos => todos, // Will override the global setting. - { inputStabilityCheck: 'always' } + { identityFunctionCheck: 'always' } ) ``` From 45f036a8ec3ffb9c50d3bba2ce8381acf2018497 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sun, 26 Nov 2023 13:29:37 -0600 Subject: [PATCH 12/24] Fix JSDocs for `shouldRunDevModeCheck` --- src/utils.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 7e9b80cdd..7ee835eaa 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -197,18 +197,15 @@ export const runIdentityFunctionCheck = (resultFunc: AnyFunction) => { } /** - * Determines if the input stability check should run. + * Determines if a development-only check should run. * - * @param inputStabilityCheck - The frequency of the input stability check. + * @param devModeCheckFrequency - The frequency of the development mode 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. + * @returns true if the development mode check should run, otherwise false. */ export const shouldRunDevModeCheck = ( - inputStabilityCheck: DevModeCheckFrequency, + devModeCheckFrequency: DevModeCheckFrequency, firstRun: boolean ) => { - return ( - inputStabilityCheck === 'always' || - (inputStabilityCheck === 'once' && firstRun) - ) + return devModeCheckFrequency === 'always' || (devModeCheckFrequency === 'once' && firstRun) } From c05bcc5426d33ae5671f7cd1115a68ffadf62bdf Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sun, 26 Nov 2023 13:33:59 -0600 Subject: [PATCH 13/24] Fix issue with `firstRun` --- src/createSelectorCreator.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/createSelectorCreator.ts b/src/createSelectorCreator.ts index 5122e614e..b726da90a 100644 --- a/src/createSelectorCreator.ts +++ b/src/createSelectorCreator.ts @@ -469,9 +469,8 @@ export function createSelectorCreator< { memoize, memoizeOptions: finalMemoizeOptions }, arguments ) - - if (firstRun) firstRun = false } + if (firstRun) firstRun = false } // apply arguments instead of spreading for performance. From 3a510952cc0b85746193e34062795a4cad71216e Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Mon, 27 Nov 2023 19:33:17 -0600 Subject: [PATCH 14/24] Add `DevModeChecks` - Add `DevModeChecks`, a type representing the configuration for development mode checks. - Replace `inputStabilityCheck` and `identityFunctionCheck` inside `CreateSelectorOptions` with `devModeChecks`. - Add `DevModeChecksExecutionInfo`, a type representing execution information for development mode checks. - Add `setGlobalDevModeChecks`, a function allowing to override `globalDevModeChecks`. - Create devModeChecks folder. - Move each dev-mode-check to their own file. - Remove `shouldRunDevModeCheck` in favor of `getDevModeChecksExecutionInfo`. - Update unit tests to comply with these changes. --- README.md | 2 +- src/createSelectorCreator.ts | 103 +------ src/devModeChecks/identityFunctionCheck.ts | 29 ++ src/devModeChecks/inputStabilityCheck.ts | 47 +++ src/devModeChecks/setGlobalDevModeChecks.ts | 62 ++++ src/index.ts | 9 +- src/types.ts | 91 ++++-- src/utils.ts | 118 ++----- test/autotrackMemoize.spec.ts | 12 +- test/defaultMemoize.spec.ts | 2 +- test/examples.test.ts | 4 +- test/identityFunctionCheck.test.ts | 72 ++++- test/inputStabilityCheck.spec.ts | 61 +++- test/reselect.spec.ts | 23 +- test/selectorUtils.spec.ts | 2 +- test/weakmapMemoize.spec.ts | 10 +- typescript_test/argsMemoize.typetest.ts | 10 +- yarn.lock | 322 ++++++++++---------- 18 files changed, 552 insertions(+), 427 deletions(-) create mode 100644 src/devModeChecks/identityFunctionCheck.ts create mode 100644 src/devModeChecks/inputStabilityCheck.ts create mode 100644 src/devModeChecks/setGlobalDevModeChecks.ts diff --git a/README.md b/README.md index 17f7ebd0c..9cb84e4a8 100644 --- a/README.md +++ b/README.md @@ -1055,7 +1055,7 @@ const selectTodoIds = createSelectorAutotrack( -### Development-Only Stability Checks +### Development-Only Checks Reselect includes extra checks in development mode to help catch and warn about mistakes in selector behavior. diff --git a/src/createSelectorCreator.ts b/src/createSelectorCreator.ts index b726da90a..92553655a 100644 --- a/src/createSelectorCreator.ts +++ b/src/createSelectorCreator.ts @@ -3,7 +3,6 @@ import { weakMapMemoize } from './weakMapMemoize' import type { Combiner, CreateSelectorOptions, - DevModeCheckFrequency, DropFirstParameter, ExtractMemoizerFields, GetParamsFromSelectors, @@ -22,9 +21,7 @@ import { collectInputSelectorResults, ensureIsArray, getDependencies, - runIdentityFunctionCheck, - runStabilityCheck, - shouldRunDevModeCheck + getDevModeChecksExecutionInfo } from './utils' /** @@ -144,90 +141,6 @@ export interface CreateSelectorFunction< InterruptRecursion } -let globalStabilityCheck: DevModeCheckFrequency = 'once' - -/** - * In development mode, an extra check is conducted on your input selectors. - * It runs your input selectors an extra time with the same arguments, and - * warns in the console if they return a different result _(based on your `memoize` method)_. - * - * 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#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._ - * - * @param inputStabilityCheckFrequency - How often the `inputStabilityCheck` should run for all selectors. - * - * @example - * ```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') - * ``` - * @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 - */ -export function setInputStabilityCheckEnabled( - inputStabilityCheckFrequency: DevModeCheckFrequency -) { - globalStabilityCheck = inputStabilityCheckFrequency -} - -let globalIdentityFunctionCheck: DevModeCheckFrequency = 'once' - -/** - * In development mode, an extra check is conducted on your result function. - * It runs your result function an extra time, and - * warns in the console if it returns its own input. - * - * 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#2-per-selector-by-passing-an-identityfunctioncheck-option-directly-to-createselector per-selector-configuration} - * and {@linkcode CreateSelectorOptions.identityFunctionCheck identityFunctionCheck} for more details. - * - * _The identity function check does not run in production builds._ - * - * @param identityFunctionCheckFrequency - How often the `identityFunctionCheck` should run for all selectors. - * - * @example - * ```ts - * import { setGlobalIdentityFunctionCheck } from 'reselect' - * - * // Run only the first time the selector is called. (default) - * setGlobalIdentityFunctionCheck('once') - * - * // Run every time the selector is called. - * setGlobalIdentityFunctionCheck('always') - * - * // Never run the identity function check. - * setGlobalIdentityFunctionCheck('never') - * ``` - * @see {@link https://github.com/reduxjs/reselect#debugging-tools debugging-tools} - * @see {@link https://github.com/reduxjs/reselect#1-globally-through-setglobalidentityfunctioncheck global-configuration} - * - * @since 5.0.0 - * @public - */ -export const setGlobalIdentityFunctionCheck = ( - identityFunctionCheckFrequency: DevModeCheckFrequency -) => { - globalIdentityFunctionCheck = identityFunctionCheckFrequency -} - /** * Creates a selector creator function with the specified memoization function and options for customizing memoization behavior. * @@ -415,8 +328,7 @@ export function createSelectorCreator< memoizeOptions = [], argsMemoize = weakMapMemoize, argsMemoizeOptions = [], - inputStabilityCheck = globalStabilityCheck, - identityFunctionCheck = globalIdentityFunctionCheck + devModeChecks = {} } = combinedOptions // Simplifying assumption: it's unlikely that the first options arg of the provided memoizer @@ -451,25 +363,28 @@ export function createSelectorCreator< ) if (process.env.NODE_ENV !== 'production') { - if (shouldRunDevModeCheck(identityFunctionCheck, firstRun)) { - runIdentityFunctionCheck( + const { identityFunctionCheck, inputStabilityCheck } = + getDevModeChecksExecutionInfo(firstRun, devModeChecks) + if (identityFunctionCheck.shouldRun) { + identityFunctionCheck.run( resultFunc as Combiner ) } - if (shouldRunDevModeCheck(inputStabilityCheck, firstRun)) { + if (inputStabilityCheck.shouldRun) { // make a second copy of the params, to check if we got the same results const inputSelectorResultsCopy = collectInputSelectorResults( dependencies, arguments ) - runStabilityCheck( + inputStabilityCheck.run( { inputSelectorResults, inputSelectorResultsCopy }, { memoize, memoizeOptions: finalMemoizeOptions }, arguments ) } + if (firstRun) firstRun = false } diff --git a/src/devModeChecks/identityFunctionCheck.ts b/src/devModeChecks/identityFunctionCheck.ts new file mode 100644 index 000000000..bbc5b6314 --- /dev/null +++ b/src/devModeChecks/identityFunctionCheck.ts @@ -0,0 +1,29 @@ +import type { AnyFunction } from '@internal/types' + +/** + * Runs a check to determine if the given result function behaves as an + * identity function. An identity function is one that returns its + * input unchanged, for example, `x => x`. This check helps ensure + * efficient memoization and prevent unnecessary re-renders by encouraging + * proper use of transformation logic in result functions and + * extraction logic in input selectors. + * + * @param resultFunc - The result function to be checked. + */ +export const runIdentityFunctionCheck = (resultFunc: AnyFunction) => { + let isInputSameAsOutput = false + try { + const emptyObject = {} + if (resultFunc(emptyObject) === emptyObject) isInputSameAsOutput = true + } catch { + // Do nothing + } + if (isInputSameAsOutput) { + console.warn( + 'The result function returned its own inputs without modification. e.g' + + '\n`createSelector([state => state.todos], todos => todos)`' + + '\nThis could lead to inefficient memoization and unnecessary re-renders.' + + '\nEnsure transformation logic is in the result function, and extraction logic is in the input selectors.' + ) + } +} diff --git a/src/devModeChecks/inputStabilityCheck.ts b/src/devModeChecks/inputStabilityCheck.ts new file mode 100644 index 000000000..285472981 --- /dev/null +++ b/src/devModeChecks/inputStabilityCheck.ts @@ -0,0 +1,47 @@ +import type { CreateSelectorOptions, UnknownMemoizer } from '@internal/types' + +/** + * Runs a stability check to ensure the input selector results remain stable + * when provided with the same arguments. This function is designed to detect + * changes in the output of input selectors, which can impact the performance of memoized selectors. + * + * @param inputSelectorResultsObject - An object containing two arrays: `inputSelectorResults` and `inputSelectorResultsCopy`, representing the results of input selectors. + * @param options - Options object consisting of a `memoize` function and a `memoizeOptions` object. + * @param inputSelectorArgs - List of arguments being passed to the input selectors. + */ +export const runInputStabilityCheck = ( + inputSelectorResultsObject: { + inputSelectorResults: unknown[] + inputSelectorResultsCopy: unknown[] + }, + options: Required< + Pick< + CreateSelectorOptions, + 'memoize' | 'memoizeOptions' + > + >, + inputSelectorArgs: unknown[] | IArguments +) => { + const { memoize, memoizeOptions } = options + const { inputSelectorResults, inputSelectorResultsCopy } = + inputSelectorResultsObject + const createAnEmptyObject = memoize(() => ({}), ...memoizeOptions) + // if the memoize method thinks the parameters are equal, these *should* be the same reference + const areInputSelectorResultsEqual = + createAnEmptyObject.apply(null, inputSelectorResults) === + createAnEmptyObject.apply(null, inputSelectorResultsCopy) + if (!areInputSelectorResultsEqual) { + // do we want to log more information about the selector? + console.warn( + 'An input selector returned a different result when passed same arguments.' + + '\nThis means your output selector will likely run more frequently than intended.' + + '\nAvoid returning a new reference inside your input selector, e.g.' + + '\n`createSelector([(arg1, arg2) => ({ arg1, arg2 })],(arg1, arg2) => {})`', + { + arguments: inputSelectorArgs, + firstInputs: inputSelectorResults, + secondInputs: inputSelectorResultsCopy + } + ) + } +} diff --git a/src/devModeChecks/setGlobalDevModeChecks.ts b/src/devModeChecks/setGlobalDevModeChecks.ts new file mode 100644 index 000000000..194823c34 --- /dev/null +++ b/src/devModeChecks/setGlobalDevModeChecks.ts @@ -0,0 +1,62 @@ +import type { DevModeChecks } from '@internal/types' + +/** + * Global configuration for development mode checks. This specifies the default + * frequency at which each development mode check should be performed. + * + * @since 5.0.0 + * @internal + */ +export const globalDevModeChecks: DevModeChecks = { + inputStabilityCheck: 'once', + identityFunctionCheck: 'once' +} + +/** + * Overrides the development mode checks settings for all selectors. + * + * Reselect performs additional checks in development mode to help identify and + * warn about potential issues in selector behavior. This function allows you to + * customize the behavior of these checks across all selectors in your application. + * + * **Note**: This setting can still be overridden per selector inside `createSelector`'s `options` object. + * See {@link https://github.com/reduxjs/reselect#2-per-selector-by-passing-an-identityfunctioncheck-option-directly-to-createselector per-selector-configuration} + * and {@linkcode CreateSelectorOptions.identityFunctionCheck identityFunctionCheck} for more details. + * + * _The development mode checks do not run in production builds._ + * + * @param devModeChecks - An object specifying the desired settings for development mode checks. You can provide partial overrides. Unspecified settings will retain their current values. + * + * @example + * ```ts + * import { setGlobalDevModeChecks } from 'reselect' + * + * // Run only the first time the selector is called. (default) + * setGlobalDevModeChecks({ inputStabilityCheck: 'once' }) + * + * // Run every time the selector is called. + * setGlobalDevModeChecks({ inputStabilityCheck: 'always' }) + * + * // Never run the input stability check. + * setGlobalDevModeChecks({ inputStabilityCheck: 'never' }) + * + * // Run only the first time the selector is called. (default) + * setGlobalDevModeChecks({ inputStabilityCheck: 'once' }) + * + * // Run every time the selector is called. + * setGlobalDevModeChecks({ inputStabilityCheck: 'always' }) + * + * // Never run the identity function check. + * setGlobalDevModeChecks({ inputStabilityCheck: 'never' }) + * ``` + * @see {@link https://github.com/reduxjs/reselect#debugging-tools debugging-tools} + * @see {@link https://github.com/reduxjs/reselect#1-globally-through-setglobalidentityfunctioncheck global-configuration} + * + * @since 5.0.0 + * @public + */ +export const setGlobalDevModeChecks = ( + devModeChecks: Partial +) => { + Object.assign(globalDevModeChecks, devModeChecks) +} diff --git a/src/index.ts b/src/index.ts index d73c33f18..51688fdf7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,5 @@ export { autotrackMemoize as unstable_autotrackMemoize } from './autotrackMemoize/autotrackMemoize' -export { - createSelector, - createSelectorCreator, - setGlobalIdentityFunctionCheck, - setInputStabilityCheckEnabled -} from './createSelectorCreator' +export { createSelector, createSelectorCreator } from './createSelectorCreator' export type { CreateSelectorFunction } from './createSelectorCreator' export { createStructuredSelector } from './createStructuredSelector' export type { @@ -18,6 +13,8 @@ export type { CreateSelectorOptions, DefaultMemoizeFields, DevModeCheckFrequency, + DevModeChecks, + DevModeChecksExecutionInfo, EqualityFn, ExtractMemoizerFields, GetParamsFromSelectors, diff --git a/src/types.ts b/src/types.ts index fbb797dec..6ead83c92 100644 --- a/src/types.ts +++ b/src/types.ts @@ -68,36 +68,15 @@ export interface CreateSelectorOptions< OverrideArgsMemoizeFunction extends UnknownMemoizer = never > { /** - * Overrides the global input stability check for the selector. - * - `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' + * Reselect performs additional checks in development mode to help identify + * and warn about potential issues in selector behavior. This option + * allows you to customize the behavior of these checks per selector. * * @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#2-per-selector-by-passing-an-inputstabilitycheck-option-directly-to-createselector per-selector-configuration} * * @since 5.0.0 */ - inputStabilityCheck?: DevModeCheckFrequency - - /** - * Overrides the global identity function check for the selector. - * - `once` - Run only the first time the selector is called. - * - `always` - Run every time the selector is called. - * - `never` - Never run the identity function check. - * - * @default 'once' - * - * @see {@link https://github.com/reduxjs/reselect#debugging-tools debugging-tools} - * @see {@link https://github.com/reduxjs/reselect#identityfunctioncheck identityFunctionCheck} - * @see {@link https://github.com/reduxjs/reselect#2-per-selector-by-passing-an-identityfunctioncheck-option-directly-to-createselector per-selector-configuration} - * - * @since 5.0.0 - */ - identityFunctionCheck?: DevModeCheckFrequency + devModeChecks?: Partial /** * The memoize function that is used to memoize the {@linkcode OutputSelectorFields.resultFunc resultFunc} @@ -314,13 +293,73 @@ export type Combiner = Distribute< export type EqualityFn = (a: T, b: T) => boolean /** - * The frequency of input stability checks. + * The frequency of development mode checks. * * @since 5.0.0 * @public */ export type DevModeCheckFrequency = 'always' | 'once' | 'never' +/** + * Represents the configuration for development mode checks. + * + * @since 5.0.0 + * @public + */ +export interface DevModeChecks { + /** + * Overrides the global input stability check for the selector. + * - `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' + * + * @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#2-per-selector-by-passing-an-inputstabilitycheck-option-directly-to-createselector per-selector-configuration} + * + * @since 5.0.0 + */ + inputStabilityCheck: DevModeCheckFrequency + + /** + * Overrides the global identity function check for the selector. + * - `once` - Run only the first time the selector is called. + * - `always` - Run every time the selector is called. + * - `never` - Never run the identity function check. + * + * @default 'once' + * + * @see {@link https://github.com/reduxjs/reselect#debugging-tools debugging-tools} + * @see {@link https://github.com/reduxjs/reselect#identityfunctioncheck identityFunctionCheck} + * @see {@link https://github.com/reduxjs/reselect#2-per-selector-by-passing-an-identityfunctioncheck-option-directly-to-createselector per-selector-configuration} + * + * @since 5.0.0 + */ + identityFunctionCheck: DevModeCheckFrequency +} + +/** + * Represents execution information for development mode checks. + * + * @public + * @since 5.0.0 + */ +export type DevModeChecksExecutionInfo = { + [K in keyof DevModeChecks]: { + /** + * A boolean indicating whether the check should be executed. + */ + shouldRun: boolean + + /** + * The function to execute for the check. + */ + run: AnyFunction + } +} + /** * Determines the combined single "State" type (first arg) from all input selectors. * diff --git a/src/utils.ts b/src/utils.ts index 7ee835eaa..88084833a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,11 +1,8 @@ -import type { - AnyFunction, - CreateSelectorOptions, - DevModeCheckFrequency, - Selector, - SelectorArray, - UnknownMemoizer -} from './types' +import { runIdentityFunctionCheck } from './devModeChecks/identityFunctionCheck' +import { runInputStabilityCheck } from './devModeChecks/inputStabilityCheck' +import { globalDevModeChecks } from './devModeChecks/setGlobalDevModeChecks' +import type { DevModeChecks, Selector, SelectorArray } from './types' +import { DevModeChecksExecutionInfo } from './types' export const NOT_FOUND = 'NOT_FOUND' export type NOT_FOUND_TYPE = typeof NOT_FOUND @@ -123,89 +120,32 @@ export function collectInputSelectorResults( } /** - * Runs a stability check to ensure the input selector results remain stable - * when provided with the same arguments. This function is designed to detect - * changes in the output of input selectors, which can impact the performance of memoized selectors. + * Retrieves execution information for development mode checks. * - * @param inputSelectorResultsObject - An object containing two arrays: `inputSelectorResults` and `inputSelectorResultsCopy`, representing the results of input selectors. - * @param options - Options object consisting of a `memoize` function and a `memoizeOptions` object. - * @param inputSelectorArgs - List of arguments being passed to the input selectors. - */ -export function runStabilityCheck( - inputSelectorResultsObject: { - inputSelectorResults: unknown[] - inputSelectorResultsCopy: unknown[] - }, - options: Required< - Pick< - CreateSelectorOptions, - 'memoize' | 'memoizeOptions' - > - >, - inputSelectorArgs: unknown[] | IArguments -) { - const { memoize, memoizeOptions } = options - const { inputSelectorResults, inputSelectorResultsCopy } = - inputSelectorResultsObject - const createAnEmptyObject = memoize(() => ({}), ...memoizeOptions) - // if the memoize method thinks the parameters are equal, these *should* be the same reference - const areInputSelectorResultsEqual = - createAnEmptyObject.apply(null, inputSelectorResults) === - createAnEmptyObject.apply(null, inputSelectorResultsCopy) - if (!areInputSelectorResultsEqual) { - // do we want to log more information about the selector? - console.warn( - 'An input selector returned a different result when passed same arguments.' + - '\nThis means your output selector will likely run more frequently than intended.' + - '\nAvoid returning a new reference inside your input selector, e.g.' + - '\n`createSelector([(arg1, arg2) => ({ arg1, arg2 })],(arg1, arg2) => {})`', - { - arguments: inputSelectorArgs, - firstInputs: inputSelectorResults, - secondInputs: inputSelectorResultsCopy - } - ) - } -} - -/** - * Runs a check to determine if the given result function behaves as an - * identity function. An identity function is one that returns its - * input unchanged, for example, `x => x`. This check helps ensure - * efficient memoization and prevent unnecessary re-renders by encouraging - * proper use of transformation logic in result functions and - * extraction logic in input selectors. - * - * @param resultFunc - The result function to be checked. - */ -export const runIdentityFunctionCheck = (resultFunc: AnyFunction) => { - let isInputSameAsOutput = false - try { - const emptyObject = {} - if (resultFunc(emptyObject) === emptyObject) isInputSameAsOutput = true - } catch { - // Do nothing - } - if (isInputSameAsOutput) { - console.warn( - 'The result function returned its own inputs without modification. e.g' + - '\n`createSelector([state => state.todos], todos => todos)`' + - '\nThis could lead to inefficient memoization and unnecessary re-renders.' + - '\nEnsure transformation logic is in the result function, and extraction logic is in the input selectors.' - ) - } -} - -/** - * Determines if a development-only check should run. - * - * @param devModeCheckFrequency - The frequency of the development mode check. + * @param devModeChecks - Custom Settings for development mode checks. These settings will override the global defaults. * @param firstRun - Indicates whether it is the first time the selector has run. - * @returns true if the development mode check should run, otherwise false. + * @returns An object containing the execution information for each development mode check. */ -export const shouldRunDevModeCheck = ( - devModeCheckFrequency: DevModeCheckFrequency, - firstRun: boolean +export const getDevModeChecksExecutionInfo = ( + firstRun: boolean, + devModeChecks: Partial ) => { - return devModeCheckFrequency === 'always' || (devModeCheckFrequency === 'once' && firstRun) + const { identityFunctionCheck, inputStabilityCheck } = { + ...globalDevModeChecks, + ...devModeChecks + } + return { + identityFunctionCheck: { + shouldRun: + identityFunctionCheck === 'always' || + (identityFunctionCheck === 'once' && firstRun), + run: runIdentityFunctionCheck + }, + inputStabilityCheck: { + shouldRun: + inputStabilityCheck === 'always' || + (inputStabilityCheck === 'once' && firstRun), + run: runInputStabilityCheck + } + } satisfies DevModeChecksExecutionInfo } diff --git a/test/autotrackMemoize.spec.ts b/test/autotrackMemoize.spec.ts index 666be5618..4e21732c5 100644 --- a/test/autotrackMemoize.spec.ts +++ b/test/autotrackMemoize.spec.ts @@ -35,7 +35,7 @@ describe('Basic selector behavior with autotrack', () => { const selector = createSelector( (state: StateA) => state.a, a => a, - { identityFunctionCheck: 'never' } + { devModeChecks: { identityFunctionCheck: 'never' } } ) const firstState = { a: 1 } const firstStateNewPointer = { a: 1 } @@ -54,7 +54,7 @@ describe('Basic selector behavior with autotrack', () => { const selector = createSelector( (...params: any[]) => params.length, a => a, - { identityFunctionCheck: 'never' } + { devModeChecks: { identityFunctionCheck: 'never' } } ) expect(selector({})).toBe(1) }) @@ -109,7 +109,7 @@ describe('Basic selector behavior with autotrack', () => { (state: StateAB) => state.a, (state: StateAB) => state.b, (a, b) => a + b, - { identityFunctionCheck: 'never' } + { devModeChecks: { identityFunctionCheck: 'never' } } ) const state1 = { a: 1, b: 2 } @@ -130,7 +130,7 @@ describe('Basic selector behavior with autotrack', () => { (state: StateAB) => state.a, (state: StateAB) => state.b, (a, b) => a + b, - { identityFunctionCheck: 'never' } + { devModeChecks: { identityFunctionCheck: 'never' } } ) const start = performance.now() @@ -197,7 +197,7 @@ describe('Basic selector behavior with autotrack', () => { called++ throw Error('test error') }, - { identityFunctionCheck: 'never' } + { devModeChecks: { identityFunctionCheck: 'never' } } ) expect(() => selector({ a: 1 })).toThrow('test error') expect(() => selector({ a: 1 })).toThrow('test error') @@ -213,7 +213,7 @@ describe('Basic selector behavior with autotrack', () => { if (a > 1) throw Error('test error') return a }, - { identityFunctionCheck: 'never' } + { devModeChecks: { identityFunctionCheck: 'never' } } ) const state1 = { a: 1 } const state2 = { a: 2 } diff --git a/test/defaultMemoize.spec.ts b/test/defaultMemoize.spec.ts index f3f754bcf..502c29d44 100644 --- a/test/defaultMemoize.spec.ts +++ b/test/defaultMemoize.spec.ts @@ -369,7 +369,7 @@ describe('defaultMemoize', () => { }, { memoizeOptions: { maxSize: 3 }, - identityFunctionCheck: 'never' + devModeChecks: { identityFunctionCheck: 'never' } } ) diff --git a/test/examples.test.ts b/test/examples.test.ts index 8f6218873..9a54026e6 100644 --- a/test/examples.test.ts +++ b/test/examples.test.ts @@ -5,10 +5,10 @@ import type { UnknownMemoizer } from 'reselect' import { + unstable_autotrackMemoize as autotrackMemoize, createSelector, createSelectorCreator, defaultMemoize, - unstable_autotrackMemoize as autotrackMemoize, weakMapMemoize } from 'reselect' import { test } from 'vitest' @@ -47,7 +47,7 @@ test('identity', () => { const nonMemoizedSelector = createNonMemoizedSelector( [(state: RootState) => state.todos], todos => todos.filter(todo => todo.completed === true), - { inputStabilityCheck: 'never' } + { devModeChecks: { inputStabilityCheck: 'never' } } ) nonMemoizedSelector(store.getState()) diff --git a/test/identityFunctionCheck.test.ts b/test/identityFunctionCheck.test.ts index 07f62c0b6..309c12633 100644 --- a/test/identityFunctionCheck.test.ts +++ b/test/identityFunctionCheck.test.ts @@ -1,8 +1,9 @@ -import { createSelector, setGlobalIdentityFunctionCheck } from 'reselect' +import { setGlobalDevModeChecks } from '@internal/devModeChecks/setGlobalDevModeChecks' +import { createSelector } from 'reselect' import type { LocalTestContext, RootState } from './testUtils' import { localTest } from './testUtils' -describe('noopCheck', () => { +describe('identityFunctionCheck', () => { const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) const identityFunction = vi.fn((state: T) => state) const badSelector = createSelector( @@ -35,20 +36,20 @@ describe('noopCheck', () => { expect(identityFunction).toHaveBeenCalledTimes(2) - expect(consoleSpy).toHaveBeenCalled() + expect(consoleSpy).toHaveBeenCalledOnce() } ) localTest('disables check if global setting is set to never', ({ state }) => { - setGlobalIdentityFunctionCheck('never') + setGlobalDevModeChecks({ identityFunctionCheck: 'never' }) expect(badSelector(state)).toBe(state) - expect(identityFunction).toHaveBeenCalledTimes(1) + expect(identityFunction).toHaveBeenCalledOnce() expect(consoleSpy).not.toHaveBeenCalled() - setGlobalIdentityFunctionCheck('once') + setGlobalDevModeChecks({ identityFunctionCheck: 'once' }) }) localTest( @@ -57,12 +58,12 @@ describe('noopCheck', () => { const badSelector = createSelector( [(state: RootState) => state], identityFunction, - { identityFunctionCheck: 'never' } + { devModeChecks: { identityFunctionCheck: 'never' } } ) expect(badSelector(state)).toBe(state) - expect(identityFunction).toHaveBeenCalledTimes(1) + expect(identityFunction).toHaveBeenCalledOnce() expect(consoleSpy).not.toHaveBeenCalled() } @@ -74,7 +75,7 @@ describe('noopCheck', () => { expect(badSelector(state)).toBe(state) - expect(identityFunction).toHaveBeenCalledTimes(1) + expect(identityFunction).toHaveBeenCalledOnce() expect(consoleSpy).not.toHaveBeenCalled() @@ -85,7 +86,54 @@ describe('noopCheck', () => { const badSelector = createSelector( [(state: RootState) => state], identityFunction, - { identityFunctionCheck: 'once' } + { devModeChecks: { identityFunctionCheck: 'once' } } + ) + expect(badSelector(state)).toBe(state) + + expect(identityFunction).toHaveBeenCalledTimes(2) + + expect(consoleSpy).toHaveBeenCalledOnce() + + const newState = { ...state } + + expect(badSelector(newState)).toBe(newState) + + expect(identityFunction).toHaveBeenCalledTimes(3) + + expect(consoleSpy).toHaveBeenCalledOnce() + }) + + localTest('allows always running the check', () => { + const badSelector = createSelector([state => state], identityFunction, { + devModeChecks: { identityFunctionCheck: 'always' } + }) + + const state = {} + + expect(badSelector(state)).toBe(state) + + expect(identityFunction).toHaveBeenCalledTimes(2) + + expect(consoleSpy).toHaveBeenCalledOnce() + + expect(badSelector({ ...state })).toStrictEqual(state) + + expect(identityFunction).toHaveBeenCalledTimes(4) + + expect(consoleSpy).toHaveBeenCalledTimes(2) + + expect(badSelector(state)).toBe(state) + + expect(identityFunction).toHaveBeenCalledTimes(6) + + expect(consoleSpy).toHaveBeenCalledTimes(3) + }) + + localTest('runs once when devModeChecks is an empty object', ({ state }) => { + const badSelector = createSelector( + [(state: RootState) => state], + identityFunction, + { devModeChecks: {} } ) expect(badSelector(state)).toBe(state) @@ -111,10 +159,10 @@ describe('noopCheck', () => { expect(identityFunction).toHaveBeenCalledTimes(2) - expect(consoleSpy).toHaveBeenCalledTimes(1) + expect(consoleSpy).toHaveBeenCalledOnce() expect(badSelector({ ...state })).not.toBe(state) - expect(consoleSpy).toHaveBeenCalledTimes(1) + expect(consoleSpy).toHaveBeenCalledOnce() }) }) diff --git a/test/inputStabilityCheck.spec.ts b/test/inputStabilityCheck.spec.ts index 3dc0d28e6..36c067fbf 100644 --- a/test/inputStabilityCheck.spec.ts +++ b/test/inputStabilityCheck.spec.ts @@ -1,8 +1,5 @@ -import { - createSelector, - defaultMemoize, - setInputStabilityCheckEnabled -} from 'reselect' +import { createSelector, defaultMemoize } from 'reselect' +import { setGlobalDevModeChecks } from '@internal/devModeChecks/setGlobalDevModeChecks' import { shallowEqual } from 'react-redux' describe('inputStabilityCheck', () => { @@ -51,25 +48,25 @@ describe('inputStabilityCheck', () => { }) it('disables check if global setting is changed', () => { - setInputStabilityCheckEnabled('never') + setGlobalDevModeChecks({ inputStabilityCheck: 'never' }) expect(addNums(1, 2)).toBe(3) - expect(unstableInput).toHaveBeenCalledTimes(1) + expect(unstableInput).toHaveBeenCalledOnce() expect(consoleSpy).not.toHaveBeenCalled() - setInputStabilityCheckEnabled('once') + setGlobalDevModeChecks({ inputStabilityCheck: 'once' }) }) it('disables check if specified in the selector options', () => { const addNums = createSelector([unstableInput], ({ a, b }) => a + b, { - inputStabilityCheck: 'never' + devModeChecks: { inputStabilityCheck: 'never' } }) expect(addNums(1, 2)).toBe(3) - expect(unstableInput).toHaveBeenCalledTimes(1) + expect(unstableInput).toHaveBeenCalledOnce() expect(consoleSpy).not.toHaveBeenCalled() }) @@ -90,7 +87,49 @@ describe('inputStabilityCheck', () => { it('allows running the check only once', () => { const addNums = createSelector([unstableInput], ({ a, b }) => a + b, { - inputStabilityCheck: 'once' + devModeChecks: { inputStabilityCheck: 'once' } + }) + + expect(addNums(1, 2)).toBe(3) + + expect(unstableInput).toHaveBeenCalledTimes(2) + + expect(consoleSpy).toHaveBeenCalledOnce() + + expect(addNums(2, 2)).toBe(4) + + expect(unstableInput).toHaveBeenCalledTimes(3) + + expect(consoleSpy).toHaveBeenCalledOnce() + }) + + it('allows always running the check', () => { + const addNums = createSelector([unstableInput], ({ a, b }) => a + b, { + devModeChecks: { inputStabilityCheck: 'always' } + }) + + expect(addNums(1, 2)).toBe(3) + + expect(unstableInput).toHaveBeenCalledTimes(2) + + expect(consoleSpy).toHaveBeenCalledOnce() + + expect(addNums(2, 2)).toBe(4) + + expect(unstableInput).toHaveBeenCalledTimes(4) + + expect(consoleSpy).toHaveBeenCalledTimes(2) + + expect(addNums(1, 2)).toBe(3) + + expect(unstableInput).toHaveBeenCalledTimes(6) + + expect(consoleSpy).toHaveBeenCalledTimes(3) + }) + + it('runs once when devModeChecks is an empty object', () => { + const addNums = createSelector([unstableInput], ({ a, b }) => a + b, { + devModeChecks: {} }) expect(addNums(1, 2)).toBe(3) diff --git a/test/reselect.spec.ts b/test/reselect.spec.ts index 8d633034c..1720dc6ba 100644 --- a/test/reselect.spec.ts +++ b/test/reselect.spec.ts @@ -48,7 +48,7 @@ describe('Basic selector behavior', () => { const selector = createSelector( (state: StateA) => state.a, a => a, - { identityFunctionCheck: 'never' } + { devModeChecks: { identityFunctionCheck: 'never' } } ) const firstState = { a: 1 } const firstStateNewPointer = { a: 1 } @@ -67,7 +67,7 @@ describe('Basic selector behavior', () => { const selector = createSelector( (...params: any[]) => params.length, a => a, - { identityFunctionCheck: 'never' } + { devModeChecks: { identityFunctionCheck: 'never' } } ) expect(selector({})).toBe(1) }) @@ -156,15 +156,16 @@ describe('Basic selector behavior', () => { expect(selector(states[0])).toBe(3) expect(selector.recomputations()).toBe(1) - // Expected a million calls to a selector with the same arguments to take less than 1 second - expect(totalTime).toBeLessThan(2000) - }) + // Expected a million calls to a selector with the same arguments to take less than 1 second + expect(totalTime).toBeLessThan(2000) + } + ) }) test('memoized composite arguments', () => { const selector = createSelector( (state: StateSub) => state.sub, sub => sub, - { identityFunctionCheck: 'never' } + { devModeChecks: { identityFunctionCheck: 'never' } } ) const state1 = { sub: { a: 1 } } expect(selector(state1)).toEqual({ a: 1 }) @@ -226,7 +227,7 @@ describe('Basic selector behavior', () => { if (a > 1) throw Error('test error') return a }, - { identityFunctionCheck: 'never' } + { devModeChecks: { identityFunctionCheck: 'never' } } ) const state1 = { a: 1 } const state2 = { a: 2 } @@ -242,7 +243,7 @@ describe('Combining selectors', () => { const selector1 = createSelector( (state: StateSub) => state.sub, sub => sub, - { identityFunctionCheck: 'never' } + { devModeChecks: { identityFunctionCheck: 'never' } } ) const selector2 = createSelector(selector1, sub => sub.a) const state1 = { sub: { a: 1 } } @@ -304,7 +305,7 @@ describe('Combining selectors', () => { const selector = createOverridenSelector( (state: StateA) => state.a, a => a, - { identityFunctionCheck: 'never' } + { devModeChecks: { identityFunctionCheck: 'never' } } ) expect(selector({ a: 1 })).toBe(1) expect(selector({ a: 2 })).toBe(1) // yes, really true @@ -408,8 +409,8 @@ describe('Customizing selectors', () => { (state: RootState) => state.todos, todos => todos.map(({ id }) => id), { - inputStabilityCheck: 'always', memoize: defaultMemoize, + devModeChecks: { inputStabilityCheck: 'always' }, memoizeOptions: { equalityCheck: (a, b) => false, resultEqualityCheck: (a, b) => false @@ -1106,7 +1107,7 @@ describe('argsMemoize and memoize', () => { users => { return users.user.details.preferences.notifications.push.frequency }, - { inputStabilityCheck: 'never' } + { devModeChecks: { inputStabilityCheck: 'never' } } ) const start = performance.now() for (let i = 0; i < 10_000_000; i++) { diff --git a/test/selectorUtils.spec.ts b/test/selectorUtils.spec.ts index 655891db1..281af2507 100644 --- a/test/selectorUtils.spec.ts +++ b/test/selectorUtils.spec.ts @@ -9,7 +9,7 @@ describe('createSelector exposed utils', () => { { memoize: defaultMemoize, argsMemoize: defaultMemoize, - identityFunctionCheck: 'never' + devModeChecks: { identityFunctionCheck: 'never' } } ) expect(selector({ a: 1 })).toBe(1) diff --git a/test/weakmapMemoize.spec.ts b/test/weakmapMemoize.spec.ts index f16c23839..a189227cc 100644 --- a/test/weakmapMemoize.spec.ts +++ b/test/weakmapMemoize.spec.ts @@ -32,7 +32,7 @@ describe('Basic selector behavior with weakMapMemoize', () => { const selector = createSelector( (state: StateA) => state.a, a => a, - { identityFunctionCheck: 'never' } + { devModeChecks: { identityFunctionCheck: 'never' } } ) const firstState = { a: 1 } const firstStateNewPointer = { a: 1 } @@ -51,7 +51,7 @@ describe('Basic selector behavior with weakMapMemoize', () => { const selector = createSelector( (...params: any[]) => params.length, a => a, - { identityFunctionCheck: 'never' } + { devModeChecks: { identityFunctionCheck: 'never' } } ) expect(selector({})).toBe(1) }) @@ -160,7 +160,7 @@ describe('Basic selector behavior with weakMapMemoize', () => { if (a > 1) throw Error('test error') return a }, - { identityFunctionCheck: 'never' } + { devModeChecks: { identityFunctionCheck: 'never' } } ) const state1 = { a: 1 } const state2 = { a: 2 } @@ -182,7 +182,7 @@ describe.skipIf(isCoverage)('weakmapMemoize performance tests', () => { (state: StateAB) => state.a, (state: StateAB) => state.b, (a, b) => a + b, - { identityFunctionCheck: 'never' } + { devModeChecks: { identityFunctionCheck: 'never' } } ) const state1 = { a: 1, b: 2 } @@ -203,7 +203,7 @@ describe.skipIf(isCoverage)('weakmapMemoize performance tests', () => { (state: StateAB) => state.a, (state: StateAB) => state.b, (a, b) => a + b, - { identityFunctionCheck: 'never' } + { devModeChecks: { identityFunctionCheck: 'never' } } ) const start = performance.now() diff --git a/typescript_test/argsMemoize.typetest.ts b/typescript_test/argsMemoize.typetest.ts index 808f4c081..c8c60ea0e 100644 --- a/typescript_test/argsMemoize.typetest.ts +++ b/typescript_test/argsMemoize.typetest.ts @@ -1,13 +1,13 @@ -import memoizeOne from 'memoize-one' -import microMemoize from 'micro-memoize' +import memoizeOne from 'memoize-one'; +import microMemoize from 'micro-memoize'; import { unstable_autotrackMemoize as autotrackMemoize, createSelector, createSelectorCreator, defaultMemoize, weakMapMemoize -} from 'reselect' -import { expectExactType } from './typesTestUtils' +} from 'reselect'; +import { expectExactType } from './typesTestUtils'; interface RootState { todos: { @@ -687,7 +687,7 @@ function overrideMemoizeAndArgsMemoizeInCreateSelector() { (id, todos) => todos.filter(todo => todo.id === id), { argsMemoize: microMemoize, - inputStabilityCheck: 'never', + devModeChecks: { inputStabilityCheck: 'never' }, memoize: memoizeOne, argsMemoizeOptions: [], memoizeOptions: [(a, b) => a === b] diff --git a/yarn.lock b/yarn.lock index 50b53803a..b536b66cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1,6 +1,3 @@ -# This file is generated by running "yarn install" inside your project. -# Manual changes might be lost - proceed with caution! - __metadata: version: 6 cacheKey: 8 @@ -56,9 +53,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/android-arm64@npm:0.19.7" +"@esbuild/android-arm64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/android-arm64@npm:0.19.8" conditions: os=android & cpu=arm64 languageName: node linkType: hard @@ -70,9 +67,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/android-arm@npm:0.19.7" +"@esbuild/android-arm@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/android-arm@npm:0.19.8" conditions: os=android & cpu=arm languageName: node linkType: hard @@ -84,9 +81,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/android-x64@npm:0.19.7" +"@esbuild/android-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/android-x64@npm:0.19.8" conditions: os=android & cpu=x64 languageName: node linkType: hard @@ -98,9 +95,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/darwin-arm64@npm:0.19.7" +"@esbuild/darwin-arm64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/darwin-arm64@npm:0.19.8" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -112,9 +109,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/darwin-x64@npm:0.19.7" +"@esbuild/darwin-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/darwin-x64@npm:0.19.8" conditions: os=darwin & cpu=x64 languageName: node linkType: hard @@ -126,9 +123,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/freebsd-arm64@npm:0.19.7" +"@esbuild/freebsd-arm64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/freebsd-arm64@npm:0.19.8" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard @@ -140,9 +137,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/freebsd-x64@npm:0.19.7" +"@esbuild/freebsd-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/freebsd-x64@npm:0.19.8" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard @@ -154,9 +151,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/linux-arm64@npm:0.19.7" +"@esbuild/linux-arm64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-arm64@npm:0.19.8" conditions: os=linux & cpu=arm64 languageName: node linkType: hard @@ -168,9 +165,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/linux-arm@npm:0.19.7" +"@esbuild/linux-arm@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-arm@npm:0.19.8" conditions: os=linux & cpu=arm languageName: node linkType: hard @@ -182,9 +179,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/linux-ia32@npm:0.19.7" +"@esbuild/linux-ia32@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-ia32@npm:0.19.8" conditions: os=linux & cpu=ia32 languageName: node linkType: hard @@ -196,9 +193,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/linux-loong64@npm:0.19.7" +"@esbuild/linux-loong64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-loong64@npm:0.19.8" conditions: os=linux & cpu=loong64 languageName: node linkType: hard @@ -210,9 +207,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/linux-mips64el@npm:0.19.7" +"@esbuild/linux-mips64el@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-mips64el@npm:0.19.8" conditions: os=linux & cpu=mips64el languageName: node linkType: hard @@ -224,9 +221,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/linux-ppc64@npm:0.19.7" +"@esbuild/linux-ppc64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-ppc64@npm:0.19.8" conditions: os=linux & cpu=ppc64 languageName: node linkType: hard @@ -238,9 +235,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/linux-riscv64@npm:0.19.7" +"@esbuild/linux-riscv64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-riscv64@npm:0.19.8" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard @@ -252,9 +249,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/linux-s390x@npm:0.19.7" +"@esbuild/linux-s390x@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-s390x@npm:0.19.8" conditions: os=linux & cpu=s390x languageName: node linkType: hard @@ -266,9 +263,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/linux-x64@npm:0.19.7" +"@esbuild/linux-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/linux-x64@npm:0.19.8" conditions: os=linux & cpu=x64 languageName: node linkType: hard @@ -280,9 +277,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/netbsd-x64@npm:0.19.7" +"@esbuild/netbsd-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/netbsd-x64@npm:0.19.8" conditions: os=netbsd & cpu=x64 languageName: node linkType: hard @@ -294,9 +291,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/openbsd-x64@npm:0.19.7" +"@esbuild/openbsd-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/openbsd-x64@npm:0.19.8" conditions: os=openbsd & cpu=x64 languageName: node linkType: hard @@ -308,9 +305,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/sunos-x64@npm:0.19.7" +"@esbuild/sunos-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/sunos-x64@npm:0.19.8" conditions: os=sunos & cpu=x64 languageName: node linkType: hard @@ -322,9 +319,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/win32-arm64@npm:0.19.7" +"@esbuild/win32-arm64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/win32-arm64@npm:0.19.8" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -336,9 +333,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/win32-ia32@npm:0.19.7" +"@esbuild/win32-ia32@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/win32-ia32@npm:0.19.8" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard @@ -350,9 +347,9 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.19.7": - version: 0.19.7 - resolution: "@esbuild/win32-x64@npm:0.19.7" +"@esbuild/win32-x64@npm:0.19.8": + version: 0.19.8 + resolution: "@esbuild/win32-x64@npm:0.19.8" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -565,86 +562,86 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.5.2" +"@rollup/rollup-android-arm-eabi@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.6.0" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-android-arm64@npm:4.5.2" +"@rollup/rollup-android-arm64@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-android-arm64@npm:4.6.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-darwin-arm64@npm:4.5.2" +"@rollup/rollup-darwin-arm64@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-darwin-arm64@npm:4.6.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-darwin-x64@npm:4.5.2" +"@rollup/rollup-darwin-x64@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-darwin-x64@npm:4.6.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.5.2" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.6.0" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.5.2" +"@rollup/rollup-linux-arm64-gnu@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.6.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.5.2" +"@rollup/rollup-linux-arm64-musl@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.6.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.5.2" +"@rollup/rollup-linux-x64-gnu@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.6.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.5.2" +"@rollup/rollup-linux-x64-musl@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.6.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.5.2" +"@rollup/rollup-win32-arm64-msvc@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.6.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.5.2" +"@rollup/rollup-win32-ia32-msvc@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.6.0" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.5.2": - version: 4.5.2 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.5.2" +"@rollup/rollup-win32-x64-msvc@npm:4.6.0": + version: 4.6.0 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.6.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -765,6 +762,17 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:*": + version: 18.2.39 + resolution: "@types/react@npm:18.2.39" + dependencies: + "@types/prop-types": "*" + "@types/scheduler": "*" + csstype: ^3.0.2 + checksum: 9bcb1f1f060f1bf8f4730fb1c7772d0323a6e707f274efee3b976c40d92af4677df4d88e9135faaacf34e13e02f92ef24eb7d0cbcf7fb75c1883f5623ccb19f4 + languageName: node + linkType: hard + "@types/react@npm:*, @types/react@npm:^18.2.38": version: 18.2.38 resolution: "@types/react@npm:18.2.38" @@ -772,7 +780,7 @@ __metadata: "@types/prop-types": "*" "@types/scheduler": "*" csstype: ^3.0.2 - checksum: 71f8c167173d32252be8b2d3c1c76b3570b94d2fbbd139da86d146be453626f5777e12c2781559119637520dbef9f91cffe968f67b5901618f29226d49fad326 + checksum: 9bcb1f1f060f1bf8f4730fb1c7772d0323a6e707f274efee3b976c40d92af4677df4d88e9135faaacf34e13e02f92ef24eb7d0cbcf7fb75c1883f5623ccb19f4 languageName: node linkType: hard @@ -1337,22 +1345,22 @@ __metadata: linkType: hard "cacache@npm:^18.0.0": - version: 18.0.0 - resolution: "cacache@npm:18.0.0" + version: 18.0.1 + resolution: "cacache@npm:18.0.1" dependencies: "@npmcli/fs": ^3.1.0 fs-minipass: ^3.0.0 glob: ^10.2.2 lru-cache: ^10.0.1 minipass: ^7.0.3 - minipass-collect: ^1.0.2 + minipass-collect: ^2.0.1 minipass-flush: ^1.0.5 minipass-pipeline: ^1.2.4 p-map: ^4.0.0 ssri: ^10.0.0 tar: ^6.1.11 unique-filename: ^3.0.0 - checksum: 2cd6bf15551abd4165acb3a4d1ef0593b3aa2fd6853ae16b5bb62199c2faecf27d36555a9545c0e07dd03347ec052e782923bdcece724a24611986aafb53e152 + checksum: 5a0b3b2ea451a0379814dc1d3c81af48c7c6db15cd8f7d72e028501ae0036a599a99bbac9687bfec307afb2760808d1c7708e9477c8c70d2b166e7d80b162a23 languageName: node linkType: hard @@ -1932,31 +1940,31 @@ __metadata: linkType: hard "esbuild@npm:^0.19.3": - version: 0.19.7 - resolution: "esbuild@npm:0.19.7" - dependencies: - "@esbuild/android-arm": 0.19.7 - "@esbuild/android-arm64": 0.19.7 - "@esbuild/android-x64": 0.19.7 - "@esbuild/darwin-arm64": 0.19.7 - "@esbuild/darwin-x64": 0.19.7 - "@esbuild/freebsd-arm64": 0.19.7 - "@esbuild/freebsd-x64": 0.19.7 - "@esbuild/linux-arm": 0.19.7 - "@esbuild/linux-arm64": 0.19.7 - "@esbuild/linux-ia32": 0.19.7 - "@esbuild/linux-loong64": 0.19.7 - "@esbuild/linux-mips64el": 0.19.7 - "@esbuild/linux-ppc64": 0.19.7 - "@esbuild/linux-riscv64": 0.19.7 - "@esbuild/linux-s390x": 0.19.7 - "@esbuild/linux-x64": 0.19.7 - "@esbuild/netbsd-x64": 0.19.7 - "@esbuild/openbsd-x64": 0.19.7 - "@esbuild/sunos-x64": 0.19.7 - "@esbuild/win32-arm64": 0.19.7 - "@esbuild/win32-ia32": 0.19.7 - "@esbuild/win32-x64": 0.19.7 + version: 0.19.8 + resolution: "esbuild@npm:0.19.8" + dependencies: + "@esbuild/android-arm": 0.19.8 + "@esbuild/android-arm64": 0.19.8 + "@esbuild/android-x64": 0.19.8 + "@esbuild/darwin-arm64": 0.19.8 + "@esbuild/darwin-x64": 0.19.8 + "@esbuild/freebsd-arm64": 0.19.8 + "@esbuild/freebsd-x64": 0.19.8 + "@esbuild/linux-arm": 0.19.8 + "@esbuild/linux-arm64": 0.19.8 + "@esbuild/linux-ia32": 0.19.8 + "@esbuild/linux-loong64": 0.19.8 + "@esbuild/linux-mips64el": 0.19.8 + "@esbuild/linux-ppc64": 0.19.8 + "@esbuild/linux-riscv64": 0.19.8 + "@esbuild/linux-s390x": 0.19.8 + "@esbuild/linux-x64": 0.19.8 + "@esbuild/netbsd-x64": 0.19.8 + "@esbuild/openbsd-x64": 0.19.8 + "@esbuild/sunos-x64": 0.19.8 + "@esbuild/win32-arm64": 0.19.8 + "@esbuild/win32-ia32": 0.19.8 + "@esbuild/win32-x64": 0.19.8 dependenciesMeta: "@esbuild/android-arm": optional: true @@ -2004,7 +2012,7 @@ __metadata: optional: true bin: esbuild: bin/esbuild - checksum: a5d979224d47ae0cc6685447eb8f1ceaf7b67f5eaeaac0246f4d589ff7d81b08e4502a6245298d948f13e9b571ac8556a6d83b084af24954f762b1cfe59dbe55 + checksum: 1dff99482ecbfcc642ec66c71e4dc5c73ce6aef68e8158a4937890b570e86a95959ac47e0f14785ba70df5a673ae4289df88a162e9759b02367ed28074cee8ba languageName: node linkType: hard @@ -3407,12 +3415,12 @@ __metadata: languageName: node linkType: hard -"minipass-collect@npm:^1.0.2": - version: 1.0.2 - resolution: "minipass-collect@npm:1.0.2" +"minipass-collect@npm:^2.0.1": + version: 2.0.1 + resolution: "minipass-collect@npm:2.0.1" dependencies: - minipass: ^3.0.0 - checksum: 14df761028f3e47293aee72888f2657695ec66bd7d09cae7ad558da30415fdc4752bbfee66287dcc6fd5e6a2fa3466d6c484dc1cbd986525d9393b9523d97f10 + minipass: ^7.0.3 + checksum: b251bceea62090f67a6cced7a446a36f4cd61ee2d5cea9aee7fff79ba8030e416327a1c5aa2908dc22629d06214b46d88fdab8c51ac76bacbf5703851b5ad342 languageName: node linkType: hard @@ -4308,21 +4316,21 @@ __metadata: linkType: hard "rollup@npm:^4.2.0": - version: 4.5.2 - resolution: "rollup@npm:4.5.2" - dependencies: - "@rollup/rollup-android-arm-eabi": 4.5.2 - "@rollup/rollup-android-arm64": 4.5.2 - "@rollup/rollup-darwin-arm64": 4.5.2 - "@rollup/rollup-darwin-x64": 4.5.2 - "@rollup/rollup-linux-arm-gnueabihf": 4.5.2 - "@rollup/rollup-linux-arm64-gnu": 4.5.2 - "@rollup/rollup-linux-arm64-musl": 4.5.2 - "@rollup/rollup-linux-x64-gnu": 4.5.2 - "@rollup/rollup-linux-x64-musl": 4.5.2 - "@rollup/rollup-win32-arm64-msvc": 4.5.2 - "@rollup/rollup-win32-ia32-msvc": 4.5.2 - "@rollup/rollup-win32-x64-msvc": 4.5.2 + version: 4.6.0 + resolution: "rollup@npm:4.6.0" + dependencies: + "@rollup/rollup-android-arm-eabi": 4.6.0 + "@rollup/rollup-android-arm64": 4.6.0 + "@rollup/rollup-darwin-arm64": 4.6.0 + "@rollup/rollup-darwin-x64": 4.6.0 + "@rollup/rollup-linux-arm-gnueabihf": 4.6.0 + "@rollup/rollup-linux-arm64-gnu": 4.6.0 + "@rollup/rollup-linux-arm64-musl": 4.6.0 + "@rollup/rollup-linux-x64-gnu": 4.6.0 + "@rollup/rollup-linux-x64-musl": 4.6.0 + "@rollup/rollup-win32-arm64-msvc": 4.6.0 + "@rollup/rollup-win32-ia32-msvc": 4.6.0 + "@rollup/rollup-win32-x64-msvc": 4.6.0 fsevents: ~2.3.2 dependenciesMeta: "@rollup/rollup-android-arm-eabi": @@ -4353,7 +4361,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 0cf68670556753c290e07492c01ea7ce19b7d178b903a518c908f7f9a90c0abee4e91c3089849f05a47040fd59c5ac0f10a58e4ee704cf7b03b24667e72c368a + checksum: f0325de87cc70086297415d2780d99f6ba7f56605aa3174ae33dff842aab11c4f5206a1718b1a4872df090589ab71a3b60fdb383b7ee59cb276ad0752c3214c7 languageName: node linkType: hard From 397f3d840f05088dfa707a2963f9a497476ca04c Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Tue, 28 Nov 2023 15:49:59 -0600 Subject: [PATCH 15/24] Update `README` with new dev-mode-check API --- README.md | 34 ++++++++++----------- src/devModeChecks/setGlobalDevModeChecks.ts | 8 ++--- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 9cb84e4a8..c8f0415ca 100644 --- a/README.md +++ b/README.md @@ -351,7 +351,7 @@ Accepts either a `memoize` function and `...memoizeOptions` rest parameter, or s | `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.devModeChecks?` | Overrides the settings for the global development mode checks for the selector.
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 | @@ -1093,23 +1093,23 @@ type DevModeCheckFrequency = 'always' | 'once' | 'never' You can configure this behavior in two ways: - + -##### 1. Globally through `setInputStabilityCheckEnabled`: +##### 1. Globally through `setGlobalDevModeChecks`: -A `setInputStabilityCheckEnabled` function is exported from Reselect, which should be called with the desired setting. +A `setGlobalDevModeChecks` function is exported from Reselect, which should be called with the desired setting. ```ts -import { setInputStabilityCheckEnabled } from 'reselect' +import { setGlobalDevModeChecks } from 'reselect' // Run only the first time the selector is called. (default) -setInputStabilityCheckEnabled('once') +setGlobalDevModeChecks({ inputStabilityCheck: 'once' }) // Run every time the selector is called. -setInputStabilityCheckEnabled('always') +setGlobalDevModeChecks({ inputStabilityCheck: 'always' }) // Never run the input stability check. -setInputStabilityCheckEnabled('never') +setGlobalDevModeChecks({ inputStabilityCheck: 'never' }) ``` ##### 2. Per selector by passing an `inputStabilityCheck` option directly to [`createSelector`]: @@ -1124,12 +1124,12 @@ const selectCompletedTodosLength = createSelector( ], completedTodos => completedTodos.length, // Will override the global setting. - { inputStabilityCheck: 'always' } + { devModeChecks: { inputStabilityCheck: 'always' } } ) ``` > [!WARNING] -> This will override the global input stability check set by calling `setInputStabilityCheckEnabled`. +> This will override the global input stability check set by calling `setGlobalDevModeChecks`. @@ -1170,21 +1170,21 @@ type DevModeCheckFrequency = 'always' | 'once' | 'never' You can configure this behavior in two ways: - + -##### 1. Globally through `setGlobalIdentityFunctionCheck`: +##### 1. Globally through `setGlobalDevModeChecks`: ```ts -import { setGlobalIdentityFunctionCheck } from 'reselect' +import { setGlobalDevModeChecks } from 'reselect' // Run only the first time the selector is called. (default) -setGlobalIdentityFunctionCheck('once') +setGlobalDevModeChecks({ identityFunctionCheck: 'once' }) // Run every time the selector is called. -setGlobalIdentityFunctionCheck('always') +setGlobalDevModeChecks({ identityFunctionCheck: 'always' }) // Never run the identity function check. -setGlobalIdentityFunctionCheck('never') +setGlobalDevModeChecks({ identityFunctionCheck: 'never' }) ``` ##### 2. Per selector by passing an `identityFunctionCheck` option directly to [`createSelector`]: @@ -1196,7 +1196,7 @@ const selectTodos = createSelector( // This result function does not contain any transformation logic. todos => todos, // Will override the global setting. - { identityFunctionCheck: 'always' } + { devModeChecks: { identityFunctionCheck: 'always' } } ) ``` diff --git a/src/devModeChecks/setGlobalDevModeChecks.ts b/src/devModeChecks/setGlobalDevModeChecks.ts index 194823c34..1cfcd5ccb 100644 --- a/src/devModeChecks/setGlobalDevModeChecks.ts +++ b/src/devModeChecks/setGlobalDevModeChecks.ts @@ -41,16 +41,16 @@ export const globalDevModeChecks: DevModeChecks = { * setGlobalDevModeChecks({ inputStabilityCheck: 'never' }) * * // Run only the first time the selector is called. (default) - * setGlobalDevModeChecks({ inputStabilityCheck: 'once' }) + * setGlobalDevModeChecks({ identityFunctionCheck: 'once' }) * * // Run every time the selector is called. - * setGlobalDevModeChecks({ inputStabilityCheck: 'always' }) + * setGlobalDevModeChecks({ identityFunctionCheck: 'always' }) * * // Never run the identity function check. - * setGlobalDevModeChecks({ inputStabilityCheck: 'never' }) + * setGlobalDevModeChecks({ identityFunctionCheck: 'never' }) * ``` * @see {@link https://github.com/reduxjs/reselect#debugging-tools debugging-tools} - * @see {@link https://github.com/reduxjs/reselect#1-globally-through-setglobalidentityfunctioncheck global-configuration} + * @see {@link https://github.com/reduxjs/reselect#1-globally-through-setglobaldevmodechecks global-configuration} * * @since 5.0.0 * @public From dda015ba275f435d2e02353429b63a09df1f1258 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Thu, 30 Nov 2023 00:17:56 -0600 Subject: [PATCH 16/24] Fix `inputStabilityCheck` warning message. --- README.md | 2 +- src/devModeChecks/inputStabilityCheck.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c8f0415ca..25c1bf560 100644 --- a/README.md +++ b/README.md @@ -1118,7 +1118,7 @@ setGlobalDevModeChecks({ inputStabilityCheck: 'never' }) // 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. + // ❌ Incorrect Use Case: This input selector will not be memoized properly since it always returns a new reference. (state: RootState) => state.todos.filter(({ completed }) => completed === true) ], diff --git a/src/devModeChecks/inputStabilityCheck.ts b/src/devModeChecks/inputStabilityCheck.ts index 285472981..6f4d87d65 100644 --- a/src/devModeChecks/inputStabilityCheck.ts +++ b/src/devModeChecks/inputStabilityCheck.ts @@ -36,7 +36,7 @@ export const runInputStabilityCheck = ( 'An input selector returned a different result when passed same arguments.' + '\nThis means your output selector will likely run more frequently than intended.' + '\nAvoid returning a new reference inside your input selector, e.g.' + - '\n`createSelector([(arg1, arg2) => ({ arg1, arg2 })],(arg1, arg2) => {})`', + '\n`createSelector([state => state.todos.map(todo => todo.id)], todoIds => todoIds.length)`', { arguments: inputSelectorArgs, firstInputs: inputSelectorResults, From df84098d4f2478bc45d141b3096dbdc5b1055f7c Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Thu, 30 Nov 2023 00:54:19 -0600 Subject: [PATCH 17/24] export `setGlobalDevModeChecks` in `index.ts` --- src/index.ts | 1 + typescript_test/argsMemoize.typetest.ts | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 51688fdf7..fe1bd6573 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ export type { } from './createStructuredSelector' export { defaultEqualityCheck, defaultMemoize } from './defaultMemoize' export type { DefaultMemoizeOptions } from './defaultMemoize' +export { setGlobalDevModeChecks } from './devModeChecks/setGlobalDevModeChecks' export type { Combiner, CreateSelectorOptions, diff --git a/typescript_test/argsMemoize.typetest.ts b/typescript_test/argsMemoize.typetest.ts index c8c60ea0e..a9f7b4c8e 100644 --- a/typescript_test/argsMemoize.typetest.ts +++ b/typescript_test/argsMemoize.typetest.ts @@ -1,13 +1,13 @@ -import memoizeOne from 'memoize-one'; -import microMemoize from 'micro-memoize'; +import memoizeOne from 'memoize-one' +import microMemoize from 'micro-memoize' import { unstable_autotrackMemoize as autotrackMemoize, createSelector, createSelectorCreator, defaultMemoize, weakMapMemoize -} from 'reselect'; -import { expectExactType } from './typesTestUtils'; +} from 'reselect' +import { expectExactType } from './typesTestUtils' interface RootState { todos: { From 17718f3a77f2ec01e0cda31d811a7e9339fcc3cc Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Thu, 30 Nov 2023 01:08:45 -0600 Subject: [PATCH 18/24] Remove '@internal' imports --- src/devModeChecks/identityFunctionCheck.ts | 2 +- src/devModeChecks/inputStabilityCheck.ts | 2 +- src/devModeChecks/setGlobalDevModeChecks.ts | 3 ++- src/versionedTypes/ts47-mergeParameters.ts | 2 +- test/identityFunctionCheck.test.ts | 7 +++---- test/inputStabilityCheck.spec.ts | 11 +++++++---- 6 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/devModeChecks/identityFunctionCheck.ts b/src/devModeChecks/identityFunctionCheck.ts index bbc5b6314..dc4e3d450 100644 --- a/src/devModeChecks/identityFunctionCheck.ts +++ b/src/devModeChecks/identityFunctionCheck.ts @@ -1,4 +1,4 @@ -import type { AnyFunction } from '@internal/types' +import type { AnyFunction } from '../types' /** * Runs a check to determine if the given result function behaves as an diff --git a/src/devModeChecks/inputStabilityCheck.ts b/src/devModeChecks/inputStabilityCheck.ts index 6f4d87d65..4f62d418e 100644 --- a/src/devModeChecks/inputStabilityCheck.ts +++ b/src/devModeChecks/inputStabilityCheck.ts @@ -1,4 +1,4 @@ -import type { CreateSelectorOptions, UnknownMemoizer } from '@internal/types' +import type { CreateSelectorOptions, UnknownMemoizer } from '../types' /** * Runs a stability check to ensure the input selector results remain stable diff --git a/src/devModeChecks/setGlobalDevModeChecks.ts b/src/devModeChecks/setGlobalDevModeChecks.ts index 1cfcd5ccb..04d5f7164 100644 --- a/src/devModeChecks/setGlobalDevModeChecks.ts +++ b/src/devModeChecks/setGlobalDevModeChecks.ts @@ -1,4 +1,4 @@ -import type { DevModeChecks } from '@internal/types' +import type { DevModeChecks } from '../types' /** * Global configuration for development mode checks. This specifies the default @@ -30,6 +30,7 @@ export const globalDevModeChecks: DevModeChecks = { * @example * ```ts * import { setGlobalDevModeChecks } from 'reselect' +import { DevModeChecks } from '../types'; * * // Run only the first time the selector is called. (default) * setGlobalDevModeChecks({ inputStabilityCheck: 'once' }) diff --git a/src/versionedTypes/ts47-mergeParameters.ts b/src/versionedTypes/ts47-mergeParameters.ts index 10092b40a..fdac93660 100644 --- a/src/versionedTypes/ts47-mergeParameters.ts +++ b/src/versionedTypes/ts47-mergeParameters.ts @@ -1,7 +1,7 @@ // This entire implementation courtesy of Anders Hjelsberg: // https://github.com/microsoft/TypeScript/pull/50831#issuecomment-1253830522 -import type { AnyFunction } from '@internal/types' +import type { AnyFunction } from '../types' /** * Represents the longest array within an array of arrays. diff --git a/test/identityFunctionCheck.test.ts b/test/identityFunctionCheck.test.ts index 309c12633..d2f60967d 100644 --- a/test/identityFunctionCheck.test.ts +++ b/test/identityFunctionCheck.test.ts @@ -1,5 +1,4 @@ -import { setGlobalDevModeChecks } from '@internal/devModeChecks/setGlobalDevModeChecks' -import { createSelector } from 'reselect' +import { createSelector, setGlobalDevModeChecks } from 'reselect' import type { LocalTestContext, RootState } from './testUtils' import { localTest } from './testUtils' @@ -124,9 +123,9 @@ describe('identityFunctionCheck', () => { expect(badSelector(state)).toBe(state) - expect(identityFunction).toHaveBeenCalledTimes(6) + expect(identityFunction).toHaveBeenCalledTimes(4) - expect(consoleSpy).toHaveBeenCalledTimes(3) + expect(consoleSpy).toHaveBeenCalledTimes(2) }) localTest('runs once when devModeChecks is an empty object', ({ state }) => { diff --git a/test/inputStabilityCheck.spec.ts b/test/inputStabilityCheck.spec.ts index 36c067fbf..f419f4387 100644 --- a/test/inputStabilityCheck.spec.ts +++ b/test/inputStabilityCheck.spec.ts @@ -1,5 +1,8 @@ -import { createSelector, defaultMemoize } from 'reselect' -import { setGlobalDevModeChecks } from '@internal/devModeChecks/setGlobalDevModeChecks' +import { + createSelector, + defaultMemoize, + setGlobalDevModeChecks +} from 'reselect' import { shallowEqual } from 'react-redux' describe('inputStabilityCheck', () => { @@ -122,9 +125,9 @@ describe('inputStabilityCheck', () => { expect(addNums(1, 2)).toBe(3) - expect(unstableInput).toHaveBeenCalledTimes(6) + expect(unstableInput).toHaveBeenCalledTimes(4) - expect(consoleSpy).toHaveBeenCalledTimes(3) + expect(consoleSpy).toHaveBeenCalledTimes(2) }) it('runs once when devModeChecks is an empty object', () => { From 4952ad418df6dcc913df3d4d518ec18d319949c7 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Thu, 30 Nov 2023 01:23:30 -0600 Subject: [PATCH 19/24] Fix `devModeChecks` in type tests --- type-tests/argsMemoize.test-d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/type-tests/argsMemoize.test-d.ts b/type-tests/argsMemoize.test-d.ts index 2ed343480..b95bcbcf1 100644 --- a/type-tests/argsMemoize.test-d.ts +++ b/type-tests/argsMemoize.test-d.ts @@ -712,7 +712,7 @@ describe('memoize and argsMemoize', () => { (id, todos) => todos.filter(todo => todo.id === id), { argsMemoize: microMemoize, - inputStabilityCheck: 'never', + devModeChecks: { inputStabilityCheck: 'never' }, memoize: memoizeOne, argsMemoizeOptions: [], memoizeOptions: [(a, b) => a === b] From 20a7d97c5459c9e7619a47871c742e40c06f848f Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Thu, 30 Nov 2023 01:58:00 -0600 Subject: [PATCH 20/24] increase performance timer from 1000 to 1500 --- test/reselect.spec.ts | 4 ++-- test/weakmapMemoize.spec.ts | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/test/reselect.spec.ts b/test/reselect.spec.ts index 1720dc6ba..15619f03a 100644 --- a/test/reselect.spec.ts +++ b/test/reselect.spec.ts @@ -3,10 +3,10 @@ import lodashMemoize from 'lodash/memoize' import microMemoize from 'micro-memoize' import { - unstable_autotrackMemoize as autotrackMemoize, createSelector, createSelectorCreator, defaultMemoize, + unstable_autotrackMemoize as autotrackMemoize, weakMapMemoize } from 'reselect' @@ -113,7 +113,7 @@ describe('Basic selector behavior', () => { const isCoverage = process.env.COVERAGE - describe('performance checks', () => { + describe.skipIf(isCoverage)('performance checks', () => { beforeAll(setEnvToProd) // don't run performance tests for coverage diff --git a/test/weakmapMemoize.spec.ts b/test/weakmapMemoize.spec.ts index a189227cc..8a68e3e17 100644 --- a/test/weakmapMemoize.spec.ts +++ b/test/weakmapMemoize.spec.ts @@ -203,7 +203,12 @@ describe.skipIf(isCoverage)('weakmapMemoize performance tests', () => { (state: StateAB) => state.a, (state: StateAB) => state.b, (a, b) => a + b, - { devModeChecks: { identityFunctionCheck: 'never' } } + { + devModeChecks: { + identityFunctionCheck: 'never', + inputStabilityCheck: 'never' + } + } ) const start = performance.now() From bc7a1888a377a3cbf15137e6d05bede1156a87fc Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Thu, 30 Nov 2023 06:43:25 -0600 Subject: [PATCH 21/24] Change package version as a workaround --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 56196ebc3..107563e02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "reselect", - "version": "5.0.0-beta.1", + "version": "5.0.0-beta.2", "description": "Selectors for Redux.", "main": "./dist/cjs/reselect.cjs", "module": "./dist/reselect.legacy-esm.js", From 01792bc6c657ba46c451d3b15d2425a99c7ae3ab Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Thu, 30 Nov 2023 22:39:38 -0500 Subject: [PATCH 22/24] Silence test logging --- test/computationComparisons.spec.tsx | 145 +++++++++++++-------------- 1 file changed, 71 insertions(+), 74 deletions(-) diff --git a/test/computationComparisons.spec.tsx b/test/computationComparisons.spec.tsx index 484306a42..a939c8588 100644 --- a/test/computationComparisons.spec.tsx +++ b/test/computationComparisons.spec.tsx @@ -9,10 +9,11 @@ import { Provider, shallowEqual, useSelector } from 'react-redux' import { createSelector, unstable_autotrackMemoize, - weakMapMemoize + weakMapMemoize, + defaultMemoize } from 'reselect' -import type { OutputSelector, defaultMemoize } from 'reselect' +import type { OutputSelector } from 'reselect' import type { RootState, Todo } from './testUtils' import { addTodo, @@ -97,7 +98,10 @@ describe('Computations and re-rendering with React components', () => { const selectTodoByIdResultEquality = createSelector( [selectTodos, selectTodoId], mapTodoById, - { memoizeOptions: { resultEqualityCheck: shallowEqual, maxSize: 500 } } + { + memoize: defaultMemoize, + memoizeOptions: { resultEqualityCheck: shallowEqual, maxSize: 500 } + } ) const selectTodoByIdWeakMap = createSelector( @@ -176,77 +180,70 @@ describe('Computations and re-rendering with React components', () => { ] ] - test.each(testCases)( - `%s`, - async ( - name, - selectTodoIds, - selectTodoById - ) => { - selectTodoIds.resetRecomputations() - selectTodoIds.resetDependencyRecomputations() - selectTodoById.resetRecomputations() - selectTodoById.resetDependencyRecomputations() - selectTodoIds.memoizedResultFunc.resetResultsCount() - selectTodoById.memoizedResultFunc.resetResultsCount() - - const numTodos = store.getState().todos.length - rtl.render( - - - - ) - - console.log(`Recomputations after render (${name}): `) - console.log('selectTodoIds: ') - logSelectorRecomputations(selectTodoIds as any) - console.log('selectTodoById: ') - logSelectorRecomputations(selectTodoById as any) - - console.log('Render count: ', { - listRenders, - listItemRenders, - listItemMounts - }) - - expect(listItemRenders).toBe(numTodos) - - rtl.act(() => { - store.dispatch(toggleCompleted(3)) - }) - - console.log(`\nRecomputations after toggle completed (${name}): `) - console.log('selectTodoIds: ') - logSelectorRecomputations(selectTodoIds as any) - console.log('selectTodoById: ') - logSelectorRecomputations(selectTodoById as any) - - console.log('Render count: ', { - listRenders, - listItemRenders, - listItemMounts - }) - - rtl.act(() => { - store.dispatch(addTodo({ title: 'a', description: 'b' })) - }) - - console.log(`\nRecomputations after added (${name}): `) - console.log('selectTodoIds: ') - logSelectorRecomputations(selectTodoIds as any) - console.log('selectTodoById: ') - logSelectorRecomputations(selectTodoById as any) - - console.log('Render count: ', { - listRenders, - listItemRenders, - listItemMounts - }) - } - ) + test.each(testCases)(`%s`, async (name, selectTodoIds, selectTodoById) => { + selectTodoIds.resetRecomputations() + selectTodoIds.resetDependencyRecomputations() + selectTodoById.resetRecomputations() + selectTodoById.resetDependencyRecomputations() + selectTodoIds.memoizedResultFunc.resetResultsCount() + selectTodoById.memoizedResultFunc.resetResultsCount() + + const numTodos = store.getState().todos.length + rtl.render( + + + + ) + + // console.log(`Recomputations after render (${name}): `) + // console.log('selectTodoIds: ') + // logSelectorRecomputations(selectTodoIds as any) + // console.log('selectTodoById: ') + // logSelectorRecomputations(selectTodoById as any) + + // console.log('Render count: ', { + // listRenders, + // listItemRenders, + // listItemMounts + // }) + + expect(listItemRenders).toBe(numTodos) + + rtl.act(() => { + store.dispatch(toggleCompleted(3)) + }) + + // console.log(`\nRecomputations after toggle completed (${name}): `) + // console.log('selectTodoIds: ') + // logSelectorRecomputations(selectTodoIds as any) + // console.log('selectTodoById: ') + // logSelectorRecomputations(selectTodoById as any) + + // console.log('Render count: ', { + // listRenders, + // listItemRenders, + // listItemMounts + // }) + + rtl.act(() => { + store.dispatch(addTodo({ title: 'a', description: 'b' })) + }) + + // console.log(`\nRecomputations after added (${name}): `) + // console.log('selectTodoIds: ') + // // logSelectorRecomputations(selectTodoIds as any) + // console.log('selectTodoById: ') + // // logSelectorRecomputations(selectTodoById as any) + + // console.log('Render count: ', { + // listRenders, + // listItemRenders, + // listItemMounts + // }) + }) }) describe('resultEqualityCheck in weakMapMemoize', () => { From 6f3bd23184547a0e0cc51f34128e5d84cb02448e Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Thu, 30 Nov 2023 22:42:52 -0500 Subject: [PATCH 23/24] Fix lockfile issue --- yarn.lock | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/yarn.lock b/yarn.lock index b536b66cd..720ad626b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1,3 +1,6 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + __metadata: version: 6 cacheKey: 8 @@ -762,7 +765,7 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*": +"@types/react@npm:*, @types/react@npm:^18.2.38": version: 18.2.39 resolution: "@types/react@npm:18.2.39" dependencies: @@ -773,17 +776,6 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:^18.2.38": - version: 18.2.38 - resolution: "@types/react@npm:18.2.38" - dependencies: - "@types/prop-types": "*" - "@types/scheduler": "*" - csstype: ^3.0.2 - checksum: 9bcb1f1f060f1bf8f4730fb1c7772d0323a6e707f274efee3b976c40d92af4677df4d88e9135faaacf34e13e02f92ef24eb7d0cbcf7fb75c1883f5623ccb19f4 - languageName: node - linkType: hard - "@types/scheduler@npm:*": version: 0.16.8 resolution: "@types/scheduler@npm:0.16.8" From ae0a9214edfb7776a13efa70f6c229d2f661b529 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Thu, 30 Nov 2023 22:45:52 -0500 Subject: [PATCH 24/24] Fix silly types import issue --- src/utils.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 88084833a..756740515 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,13 @@ import { runIdentityFunctionCheck } from './devModeChecks/identityFunctionCheck' import { runInputStabilityCheck } from './devModeChecks/inputStabilityCheck' import { globalDevModeChecks } from './devModeChecks/setGlobalDevModeChecks' -import type { DevModeChecks, Selector, SelectorArray } from './types' -import { DevModeChecksExecutionInfo } from './types' +// eslint-disable-next-line @typescript-eslint/consistent-type-imports +import type { + DevModeChecks, + Selector, + SelectorArray, + DevModeChecksExecutionInfo +} from './types' export const NOT_FOUND = 'NOT_FOUND' export type NOT_FOUND_TYPE = typeof NOT_FOUND