diff --git a/docs/examples/createSelector/annotateResultFunction.ts b/docs/examples/createSelector/annotateResultFunction.ts new file mode 100644 index 000000000..62b5cdc14 --- /dev/null +++ b/docs/examples/createSelector/annotateResultFunction.ts @@ -0,0 +1,28 @@ +import { createSelector } from 'reselect' + +interface Todo { + id: number + completed: boolean +} + +interface Alert { + id: number + read: boolean +} + +export interface RootState { + todos: Todo[] + alerts: Alert[] +} + +export const createAppSelector = createSelector.withTypes() + +const selectTodoIds = createAppSelector( + // Type of `state` is set to `RootState`, no need to manually set the type + state => state.todos, + // ❌ Known limitation: Parameter types are not inferred in this scenario + // so you will have to manually annotate them. + // highlight-start + (todos: Todo[]) => todos.map(({ id }) => id) + // highlight-end +) diff --git a/docs/examples/createSelector/createAppSelector.ts b/docs/examples/createSelector/createAppSelector.ts new file mode 100644 index 000000000..52ac14863 --- /dev/null +++ b/docs/examples/createSelector/createAppSelector.ts @@ -0,0 +1,36 @@ +import microMemoize from 'micro-memoize' +import { shallowEqual } from 'react-redux' +import { createSelectorCreator, lruMemoize } from 'reselect' + +export interface RootState { + todos: { id: number; completed: boolean }[] + alerts: { id: number; read: boolean }[] +} + +export const createAppSelector = createSelectorCreator({ + memoize: lruMemoize, + argsMemoize: microMemoize, + memoizeOptions: { + maxSize: 10, + equalityCheck: shallowEqual, + resultEqualityCheck: shallowEqual + }, + argsMemoizeOptions: { + isEqual: shallowEqual, + maxSize: 10 + }, + devModeChecks: { + identityFunctionCheck: 'never', + inputStabilityCheck: 'always' + } +}).withTypes() + +const selectReadAlerts = createAppSelector( + [ + // Type of `state` is set to `RootState`, no need to manually set the type + // highlight-start + state => state.alerts + // highlight-end + ], + alerts => alerts.filter(({ read }) => read) +) diff --git a/docs/examples/createSelector/withTypes.ts b/docs/examples/createSelector/withTypes.ts new file mode 100644 index 000000000..dd00e745d --- /dev/null +++ b/docs/examples/createSelector/withTypes.ts @@ -0,0 +1,18 @@ +import { createSelector } from 'reselect' + +export interface RootState { + todos: { id: number; completed: boolean }[] + alerts: { id: number; read: boolean }[] +} + +export const createAppSelector = createSelector.withTypes() + +const selectTodoIds = createAppSelector( + [ + // Type of `state` is set to `RootState`, no need to manually set the type + // highlight-start + state => state.todos + // highlight-end + ], + todos => todos.map(({ id }) => id) +) diff --git a/src/createSelectorCreator.ts b/src/createSelectorCreator.ts index c5b7fec5b..aed72dd34 100644 --- a/src/createSelectorCreator.ts +++ b/src/createSelectorCreator.ts @@ -29,12 +29,14 @@ import { * * @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `lruMemoize` or `weakMapMemoize`). * @template ArgsMemoizeFunction - The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createSelector` (e.g., `lruMemoize` or `weakMapMemoize`). If none is explicitly provided, `weakMapMemoize` will be used. + * @template StateType - The type of state that the selectors created with this selector creator will operate on. * * @public */ export interface CreateSelectorFunction< MemoizeFunction extends UnknownMemoizer = typeof weakMapMemoize, - ArgsMemoizeFunction extends UnknownMemoizer = typeof weakMapMemoize + ArgsMemoizeFunction extends UnknownMemoizer = typeof weakMapMemoize, + StateType = any > { /** * Creates a memoized selector function. @@ -47,9 +49,9 @@ export interface CreateSelectorFunction< * @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`. * @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`. * - * @see {@link https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-createselectoroptions createSelector} + * @see {@link https://reselect.js.org/api/createselector `createSelector`} */ - ( + , Result>( ...createSelectorArgs: [ ...inputSelectors: InputSelectors, combiner: Combiner @@ -73,10 +75,10 @@ export interface CreateSelectorFunction< * @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`. * @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`. * - * @see {@link https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-createselectoroptions createSelector} + * @see {@link https://reselect.js.org/api/createselector `createSelector`} */ < - InputSelectors extends SelectorArray, + InputSelectors extends SelectorArray, Result, OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction, OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction @@ -114,10 +116,10 @@ export interface CreateSelectorFunction< * @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`. * @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`. * - * @see {@link https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-createselectoroptions createSelector} + * @see {@link https://reselect.js.org/api/createselector `createSelector`} */ < - InputSelectors extends SelectorArray, + InputSelectors extends SelectorArray, Result, OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction, OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction @@ -139,6 +141,46 @@ export interface CreateSelectorFunction< OverrideArgsMemoizeFunction > & InterruptRecursion + + /** + * Creates a "pre-typed" version of {@linkcode createSelector createSelector} + * where the `state` type is predefined. + * + * This allows you to set the `state` type once, eliminating the need to + * specify it with every {@linkcode createSelector createSelector} call. + * + * @returns A pre-typed `createSelector` with the state type already defined. + * + * @example + * ```ts + * import { createSelector } from 'reselect' + * + * export interface RootState { + * todos: { id: number; completed: boolean }[] + * alerts: { id: number; read: boolean }[] + * } + * + * export const createAppSelector = createSelector.withTypes() + * + * const selectTodoIds = createAppSelector( + * [ + * // Type of `state` is set to `RootState`, no need to manually set the type + * state => state.todos + * ], + * todos => todos.map(({ id }) => id) + * ) + * ``` + * @template OverrideStateType - The specific type of state used by all selectors created with this selector creator. + * + * @see {@link https://reselect.js.org/api/createselector#defining-a-pre-typed-createselector `createSelector.withTypes`} + * + * @since 5.0.2 + */ + withTypes(): CreateSelectorFunction< + MemoizeFunction, + ArgsMemoizeFunction, + OverrideStateType + > } /** @@ -226,7 +268,8 @@ export function createSelectorCreator( ): CreateSelectorFunction /** - * Creates a selector creator function with the specified memoization function and options for customizing memoization behavior. + * Creates a selector creator function with the specified memoization + * function and options for customizing memoization behavior. * * @param memoizeOrOptions - Either A `memoize` function or an `options` object containing the `memoize` function. * @param memoizeOptionsFromArgs - Optional configuration options for the memoization function. These options are then passed to the memoize function as the second argument onwards. @@ -426,6 +469,11 @@ export function createSelectorCreator< OverrideArgsMemoizeFunction > } + + Object.assign(createSelector, { + withTypes: () => createSelector + }) + return createSelector as CreateSelectorFunction< MemoizeFunction, ArgsMemoizeFunction diff --git a/src/types.ts b/src/types.ts index dc7ca0015..742257a8b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -41,7 +41,7 @@ export type Selector< * * @public */ -export type SelectorArray = readonly Selector[] +export type SelectorArray = readonly Selector[] /** * Extracts an array of all return types from all input selectors. diff --git a/test/createSelector.withTypes.test.ts b/test/createSelector.withTypes.test.ts new file mode 100644 index 000000000..2cf3e7b99 --- /dev/null +++ b/test/createSelector.withTypes.test.ts @@ -0,0 +1,23 @@ +import { createSelector } from 'reselect' +import type { RootState } from './testUtils' +import { localTest } from './testUtils' + +describe(createSelector.withTypes, () => { + const createTypedSelector = createSelector.withTypes() + + localTest('should return createSelector', ({ state }) => { + expect(createTypedSelector.withTypes).to.be.a('function') + + expect(createTypedSelector.withTypes().withTypes).to.be.a('function') + + expect(createTypedSelector).toBe(createSelector) + + const selectTodoIds = createTypedSelector([state => state.todos], todos => + todos.map(({ id }) => id) + ) + + expect(selectTodoIds).toBeMemoizedSelector() + + expect(selectTodoIds(state)).to.be.an('array').that.is.not.empty + }) +}) diff --git a/test/customMatchers.d.ts b/test/customMatchers.d.ts new file mode 100644 index 000000000..961e18138 --- /dev/null +++ b/test/customMatchers.d.ts @@ -0,0 +1,10 @@ +import type { Assertion, AsymmetricMatchersContaining } from 'vitest' + +interface CustomMatchers { + toBeMemoizedSelector(): R +} + +declare module 'vitest' { + interface Assertion extends CustomMatchers {} + interface AsymmetricMatchersContaining extends CustomMatchers {} +} diff --git a/test/reselect.spec.ts b/test/reselect.spec.ts index 69734ba71..a11767a41 100644 --- a/test/reselect.spec.ts +++ b/test/reselect.spec.ts @@ -15,6 +15,7 @@ import type { RootState } from './testUtils' import { addTodo, deepClone, + isMemoizedSelector, localTest, setEnvToProd, toggleCompleted @@ -479,43 +480,9 @@ describe('argsMemoize and memoize', () => { const hasUndefinedValues = (object: object) => { return Object.values(object).some(e => e == null) } - const isMemoizedSelector = (selector: object) => { - return ( - typeof selector === 'function' && - 'resultFunc' in selector && - 'memoizedResultFunc' in selector && - 'lastResult' in selector && - 'dependencies' in selector && - 'recomputations' in selector && - 'dependencyRecomputations' in selector && - 'resetRecomputations' in selector && - 'resetDependencyRecomputations' in selector && - 'memoize' in selector && - 'argsMemoize' in selector && - typeof selector.resultFunc === 'function' && - typeof selector.memoizedResultFunc === 'function' && - typeof selector.lastResult === 'function' && - Array.isArray(selector.dependencies) && - typeof selector.recomputations === 'function' && - typeof selector.dependencyRecomputations === 'function' && - typeof selector.resetRecomputations === 'function' && - typeof selector.resetDependencyRecomputations === 'function' && - typeof selector.memoize === 'function' && - typeof selector.argsMemoize === 'function' && - selector.dependencies.length >= 1 && - selector.dependencies.every( - (dependency): dependency is Function => - typeof dependency === 'function' - ) && - !selector.lastResult.length && - !selector.recomputations.length && - !selector.resetRecomputations.length && - typeof selector.recomputations() === 'number' - ) - } const isArrayOfFunctions = (array: any[]) => array.every(e => typeof e === 'function') - expect(selectorDefault).toSatisfy(isMemoizedSelector) + expect(selectorDefault).toBeMemoizedSelector() expect(selectorDefault) .to.be.a('function') .that.has.all.keys(allFields) diff --git a/test/setup.vitest.ts b/test/setup.vitest.ts new file mode 100644 index 000000000..b4e4f2746 --- /dev/null +++ b/test/setup.vitest.ts @@ -0,0 +1,12 @@ +import { isMemoizedSelector } from './testUtils' + +expect.extend({ + toBeMemoizedSelector(received) { + const { isNot } = this + + return { + pass: isMemoizedSelector(received), + message: () => `${received} is${isNot ? '' : ' not'} a memoized selector` + } + } +}) diff --git a/test/testUtils.ts b/test/testUtils.ts index 8604d1a7b..5a9ea9258 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -523,3 +523,38 @@ export const setEnvToProd = () => { process.env.NODE_ENV = originalEnv } } + +export const isMemoizedSelector = (selector: object) => { + return ( + typeof selector === 'function' && + 'resultFunc' in selector && + 'memoizedResultFunc' in selector && + 'lastResult' in selector && + 'dependencies' in selector && + 'recomputations' in selector && + 'dependencyRecomputations' in selector && + 'resetRecomputations' in selector && + 'resetDependencyRecomputations' in selector && + 'memoize' in selector && + 'argsMemoize' in selector && + typeof selector.resultFunc === 'function' && + typeof selector.memoizedResultFunc === 'function' && + typeof selector.lastResult === 'function' && + Array.isArray(selector.dependencies) && + typeof selector.recomputations === 'function' && + typeof selector.dependencyRecomputations === 'function' && + typeof selector.resetRecomputations === 'function' && + typeof selector.resetDependencyRecomputations === 'function' && + typeof selector.memoize === 'function' && + typeof selector.argsMemoize === 'function' && + selector.dependencies.length >= 1 && + selector.dependencies.every( + (dependency): dependency is Function => typeof dependency === 'function' + ) && + !selector.lastResult.length && + !selector.recomputations.length && + !selector.resetRecomputations.length && + typeof selector.recomputations() === 'number' && + typeof selector.dependencyRecomputations() === 'number' + ) +} diff --git a/test/tsconfig.json b/test/tsconfig.json index edc9563e7..0d98c80c8 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -11,7 +11,7 @@ "target": "ESNext", "jsx": "react", "baseUrl": ".", - "rootDir": ".", + "rootDir": "../", "skipLibCheck": true, "noImplicitReturns": false, "noUnusedLocals": false, @@ -21,7 +21,5 @@ "@internal/*": ["../src/*"] } }, - "include": [ - "**/*.ts*", - ] + "include": ["**/*.ts*"] } diff --git a/type-tests/createSelector.withTypes.test-d.ts b/type-tests/createSelector.withTypes.test-d.ts new file mode 100644 index 000000000..c3e00618e --- /dev/null +++ b/type-tests/createSelector.withTypes.test-d.ts @@ -0,0 +1,118 @@ +import { createSelector } from 'reselect' +import { describe, expectTypeOf, test } from 'vitest' + +interface Todo { + id: number + completed: boolean +} + +interface Alert { + id: number + read: boolean +} + +interface RootState { + todos: Todo[] + alerts: Alert[] +} + +const rootState: RootState = { + todos: [ + { id: 0, completed: false }, + { id: 1, completed: false } + ], + alerts: [ + { id: 0, read: false }, + { id: 1, read: false } + ] +} + +describe('createSelector.withTypes()', () => { + const createAppSelector = createSelector.withTypes() + + describe('when input selectors are provided as a single array', () => { + test('locks down state type and infers result function parameter types correctly', () => { + expectTypeOf(createSelector.withTypes).returns.toEqualTypeOf( + createSelector + ) + + // Type of state is locked and the parameter types of the result function + // are correctly inferred when input selectors are provided as a single array. + createAppSelector( + [ + state => { + expectTypeOf(state).toEqualTypeOf(rootState) + + return state.todos + } + ], + todos => { + expectTypeOf(todos).toEqualTypeOf(rootState.todos) + + return todos.map(({ id }) => id) + } + ) + }) + }) + + describe('when input selectors are provided as separate inline arguments', () => { + test('locks down state type but does not infer result function parameter types', () => { + // Type of state is locked but the parameter types of the + // result function are NOT correctly inferred when + // input selectors are provided as separate inline arguments. + createAppSelector( + state => { + expectTypeOf(state).toEqualTypeOf(rootState) + + return state.todos + }, + todos => { + // Known limitation: Parameter types are not inferred in this scenario + expectTypeOf(todos).toBeAny() + + expectTypeOf(todos).not.toEqualTypeOf(rootState.todos) + + // @ts-expect-error A typed `createSelector` currently only infers + // the parameter types of the result function when + // input selectors are provided as a single array. + return todos.map(({ id }) => id) + } + ) + }) + + test('handles multiple input selectors with separate inline arguments', () => { + // Checking to see if the type of state is correct when multiple + // input selectors are provided as separate inline arguments. + createAppSelector( + state => { + expectTypeOf(state).toEqualTypeOf(rootState) + + return state.todos + }, + state => { + expectTypeOf(state).toEqualTypeOf(rootState) + + return state.alerts + }, + (todos, alerts) => { + // Known limitation: Parameter types are not inferred in this scenario + expectTypeOf(todos).toBeAny() + + expectTypeOf(alerts).toBeAny() + + // @ts-expect-error A typed `createSelector` currently only infers + // the parameter types of the result function when + // input selectors are provided as a single array. + return todos.map(({ id }) => id) + } + ) + }) + + test('can annotate parameter types of the result function to workaround type inference issue', () => { + createAppSelector( + state => state.todos, + (todos: Todo[]) => todos.map(({ id }) => id) + ) + }) + }) +}) diff --git a/type-tests/tsconfig.json b/type-tests/tsconfig.json index a8a65e555..4fae7b4d6 100644 --- a/type-tests/tsconfig.json +++ b/type-tests/tsconfig.json @@ -12,5 +12,6 @@ "reselect": ["../src/index"], // @remap-prod-remove-line "@internal/*": ["../src/*"] } - } + }, + "include": ["**/*.ts", "../typescript_test/**/*.ts"] } diff --git a/typescript_test/argsMemoize.typetest.ts b/typescript_test/argsMemoize.typetest.ts index 4e58b6a71..1e883ab99 100644 --- a/typescript_test/argsMemoize.typetest.ts +++ b/typescript_test/argsMemoize.typetest.ts @@ -832,7 +832,7 @@ function memoizeAndArgsMemoizeInCreateSelectorCreator() { ) const createSelectorWithWrongArgsMemoizeOptions = - // @ts-expect-error If we don't pass in `argsMemoize`, the type for `argsMemoizeOptions` falls back to the options parameter of `lruMemoize`. + // @ts-expect-error If we don't pass in `argsMemoize`, the type for `argsMemoizeOptions` falls back to the options parameter of `weakMapMemoize`. createSelectorCreator({ memoize: microMemoize, memoizeOptions: { isEqual: (a, b) => a === b }, diff --git a/vitest.config.ts b/vitest.config.ts index 305599842..80de86fbd 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -12,6 +12,7 @@ export default defineConfig({ typecheck: { tsconfig: './type-tests/tsconfig.json' }, globals: true, include: ['./test/**/*.(spec|test).[jt]s?(x)'], + setupFiles: ['test/setup.vitest.ts'], alias: { reselect: path.join(__dirname, 'src/index.ts'), // @remap-prod-remove-line diff --git a/website/docs/FAQ.mdx b/website/docs/FAQ.mdx index 32a76d0ab..ab98c197e 100644 --- a/website/docs/FAQ.mdx +++ b/website/docs/FAQ.mdx @@ -26,7 +26,7 @@ Keep an eye on the `dependencyRecomputations` count. If it increases while `reco To delve deeper, you can determine which arguments are changing references too frequently by using the `argsMemoizeOptions` and `equalityCheck`. Consider the following example: - +{/* START: FAQ/selectorRecomputing.ts */} - +{/* END: FAQ/selectorRecomputing.ts */} ## Can I use Reselect without Redux? @@ -192,7 +192,7 @@ Yes. The built-in `lruMemoize` memoizer works great for a lot of use cases, but Selectors are pure functions - for a given input, a selector should always produce the same result. For this reason they are simple to unit test: call the selector with a set of inputs, and assert that the result value matches an expected shape. - +{/* START: FAQ/howToTest.test.ts */} { - +{/* END: FAQ/howToTest.test.ts */} ## Can I share a selector across multiple component instances? @@ -361,7 +361,7 @@ function TodosList({ category }) { If you prefer to use a curried form instead, you can create a curried selector with this recipe: - +{/* START: FAQ/currySelector.ts */} - +{/* END: FAQ/currySelector.ts */} Or for reusability you can do this: - +{/* START: FAQ/createCurriedSelector.ts */} { - +{/* END: FAQ/createCurriedSelector.ts */} This: @@ -512,7 +512,7 @@ const todoById = useSelector(selectTodoByIdCurried(id)) Another thing you can do if you are using is create a custom hook factory function: - +{/* START: FAQ/createParametricSelectorHook.ts */} - +{/* END: FAQ/createParametricSelectorHook.ts */} And then inside your component: - +{/* START: FAQ/MyComponent.tsx */} { - +{/* END: FAQ/MyComponent.tsx */} ## How can I make a pre-typed version of `createSelector` for my root state? @@ -728,7 +728,7 @@ This approach currently only supports provided There may be rare cases when you might want to use `createSelector` for its composition syntax, but without any memoization applied. In that case, create an and use it as the memoizers: - +{/* START: FAQ/identity.ts */} - +{/* END: FAQ/identity.ts */} diff --git a/website/docs/api/createSelector.mdx b/website/docs/api/createSelector.mdx index 37895b545..3d70d80e6 100644 --- a/website/docs/api/createSelector.mdx +++ b/website/docs/api/createSelector.mdx @@ -6,6 +6,9 @@ hide_title: true description: 'createSelector' --- +import Tabs from '@theme/Tabs' +import TabItem from '@theme/TabItem' + import { InternalLinks } from '@site/src/components/InternalLinks' # `createSelector` @@ -67,3 +70,243 @@ The output selectors created by `createSelector` have several additional propert | `Result` | The return type of the as well as the . | | `OverrideMemoizeFunction` | The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into . | | `OverrideArgsMemoizeFunction` | The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into . | + +## Defining a Pre-Typed `createSelector` + +As of Reselect 5.0.2, you can create a "pre-typed" version of where the `state` type is predefined. This allows you to set the `state` type once, eliminating the need to specify it with every call. + +To do this, you can call `createSelector.withTypes()`: + +{/* START: createSelector/withTypes.ts */} + + + + +```ts title="createSelector/withTypes.ts" +import { createSelector } from 'reselect' + +export interface RootState { + todos: { id: number; completed: boolean }[] + alerts: { id: number; read: boolean }[] +} + +export const createAppSelector = createSelector.withTypes() + +const selectTodoIds = createAppSelector( + [ + // Type of `state` is set to `RootState`, no need to manually set the type + // highlight-start + state => state.todos + // highlight-end + ], + todos => todos.map(({ id }) => id) +) +``` + + + + +```js title="createSelector/withTypes.js" +import { createSelector } from 'reselect' + +export const createAppSelector = createSelector.withTypes() + +const selectTodoIds = createAppSelector( + [ + // Type of `state` is set to `RootState`, no need to manually set the type + // highlight-start + state => state.todos + // highlight-end + ], + todos => todos.map(({ id }) => id) +) +``` + + + + +{/* END: createSelector/withTypes.ts */} + +Import and use the pre-typed `createAppSelector` instead of the original, and the type for state will be used automatically. + +:::danger Known Limitations + +Currently this approach only works if input selectors are provided as a single array. + +If you pass the input selectors as separate inline arguments, the parameter types of the result function will not be inferred. +As a workaround you can either + +1. Wrap your input selectors in a single array +2. You can annotate the parameter types of the result function: + +{/* START: createSelector/annotateResultFunction.ts */} + + + + +```ts title="createSelector/annotateResultFunction.ts" +import { createSelector } from 'reselect' + +interface Todo { + id: number + completed: boolean +} + +interface Alert { + id: number + read: boolean +} + +export interface RootState { + todos: Todo[] + alerts: Alert[] +} + +export const createAppSelector = createSelector.withTypes() + +const selectTodoIds = createAppSelector( + // Type of `state` is set to `RootState`, no need to manually set the type + state => state.todos, + // ❌ Known limitation: Parameter types are not inferred in this scenario + // so you will have to manually annotate them. + // highlight-start + (todos: Todo[]) => todos.map(({ id }) => id) + // highlight-end +) +``` + + + + +```js title="createSelector/annotateResultFunction.js" +import { createSelector } from 'reselect' + +export const createAppSelector = createSelector.withTypes() + +const selectTodoIds = createAppSelector( + // Type of `state` is set to `RootState`, no need to manually set the type + state => state.todos, + // ❌ Known limitation: Parameter types are not inferred in this scenario + // so you will have to manually annotate them. + // highlight-start + todos => todos.map(({ id }) => id) + // highlight-end +) +``` + + + + +{/* END: createSelector/annotateResultFunction.ts */} + +::: + +:::tip + +You can also use this API with to create a pre-typed custom selector creator: + +{/* START: createSelector/createAppSelector.ts */} + + + + +```ts title="createSelector/createAppSelector.ts" +import microMemoize from 'micro-memoize' +import { shallowEqual } from 'react-redux' +import { createSelectorCreator, lruMemoize } from 'reselect' + +export interface RootState { + todos: { id: number; completed: boolean }[] + alerts: { id: number; read: boolean }[] +} + +export const createAppSelector = createSelectorCreator({ + memoize: lruMemoize, + argsMemoize: microMemoize, + memoizeOptions: { + maxSize: 10, + equalityCheck: shallowEqual, + resultEqualityCheck: shallowEqual + }, + argsMemoizeOptions: { + isEqual: shallowEqual, + maxSize: 10 + }, + devModeChecks: { + identityFunctionCheck: 'never', + inputStabilityCheck: 'always' + } +}).withTypes() + +const selectReadAlerts = createAppSelector( + [ + // Type of `state` is set to `RootState`, no need to manually set the type + // highlight-start + state => state.alerts + // highlight-end + ], + alerts => alerts.filter(({ read }) => read) +) +``` + + + + +```js title="createSelector/createAppSelector.js" +import microMemoize from 'micro-memoize' +import { shallowEqual } from 'react-redux' +import { createSelectorCreator, lruMemoize } from 'reselect' + +export const createAppSelector = createSelectorCreator({ + memoize: lruMemoize, + argsMemoize: microMemoize, + memoizeOptions: { + maxSize: 10, + equalityCheck: shallowEqual, + resultEqualityCheck: shallowEqual + }, + argsMemoizeOptions: { + isEqual: shallowEqual, + maxSize: 10 + }, + devModeChecks: { + identityFunctionCheck: 'never', + inputStabilityCheck: 'always' + } +}).withTypes() + +const selectReadAlerts = createAppSelector( + [ + // Type of `state` is set to `RootState`, no need to manually set the type + // highlight-start + state => state.alerts + // highlight-end + ], + alerts => alerts.filter(({ read }) => read) +) +``` + + + + +{/* END: createSelector/createAppSelector.ts */} + +::: diff --git a/website/docs/api/createStructuredSelector.mdx b/website/docs/api/createStructuredSelector.mdx index 96012f23f..c5a16156a 100644 --- a/website/docs/api/createStructuredSelector.mdx +++ b/website/docs/api/createStructuredSelector.mdx @@ -37,7 +37,7 @@ A memoized structured selector. ### Modern Use Case - +{/* START: createStructuredSelector/modernUseCase.ts */} - +{/* END: createStructuredSelector/modernUseCase.ts */} In your component: - +{/* START: createStructuredSelector/MyComponent.tsx */} { - +{/* END: createStructuredSelector/MyComponent.tsx */} ### Simple Use Case diff --git a/website/docs/api/development-only-stability-checks.mdx b/website/docs/api/development-only-stability-checks.mdx index 2b6d81aed..b6b0935f7 100644 --- a/website/docs/api/development-only-stability-checks.mdx +++ b/website/docs/api/development-only-stability-checks.mdx @@ -69,7 +69,7 @@ setGlobalDevModeChecks({ inputStabilityCheck: 'never' }) ### 2. Per selector by passing an `inputStabilityCheck` option directly to `createSelector`: - +{/* START: development-only-stability-checks/inputStabilityCheck.ts */} - +{/* END: development-only-stability-checks/inputStabilityCheck.ts */} :::warning @@ -189,7 +189,7 @@ setGlobalDevModeChecks({ identityFunctionCheck: 'never' }) ### 2. Per selector by passing an `identityFunctionCheck` option directly to `createSelector`: - +{/* START: development-only-stability-checks/identityFunctionCheck.ts */} - +{/* END: development-only-stability-checks/identityFunctionCheck.ts */} :::warning diff --git a/website/docs/api/lruMemoize.mdx b/website/docs/api/lruMemoize.mdx index 6a400fbb6..3d8df1f34 100644 --- a/website/docs/api/lruMemoize.mdx +++ b/website/docs/api/lruMemoize.mdx @@ -19,7 +19,7 @@ It has a default cache size of 1. This means it always recalculates when the val It determines if an argument has changed by calling the `equalityCheck` function. As `lruMemoize` is designed to be used with immutable data, the default `equalityCheck` function checks for changes using : - +{/* START: lruMemoize/referenceEqualityCheck.ts */} { - +{/* END: lruMemoize/referenceEqualityCheck.ts */} ## Parameters @@ -95,7 +95,7 @@ A memoized function with a `.clearCache()` method attached. ### Using `lruMemoize` with `createSelector` - +{/* START: lruMemoize/usingWithCreateSelector.ts */} - +{/* END: lruMemoize/usingWithCreateSelector.ts */} ### Using `lruMemoize` with `createSelectorCreator` - +{/* START: lruMemoize/usingWithCreateSelectorCreator.ts */} - +{/* END: lruMemoize/usingWithCreateSelectorCreator.ts */} diff --git a/website/docs/api/unstable_autotrackMemoize.mdx b/website/docs/api/unstable_autotrackMemoize.mdx index e6592dbaa..2d46e5139 100644 --- a/website/docs/api/unstable_autotrackMemoize.mdx +++ b/website/docs/api/unstable_autotrackMemoize.mdx @@ -62,7 +62,7 @@ A memoized function with a `.clearCache()` method attached. ### Using `unstable_autotrackMemoize` with `createSelector` - +{/* START: unstable_autotrackMemoize/usingWithCreateSelector.ts */} - +{/* END: unstable_autotrackMemoize/usingWithCreateSelector.ts */} ### Using `unstable_autotrackMemoize` with `createSelectorCreator` - +{/* START: unstable_autotrackMemoize/usingWithCreateSelectorCreator.ts */} state.todos], todos => - +{/* END: unstable_autotrackMemoize/usingWithCreateSelectorCreator.ts */} diff --git a/website/docs/api/weakMapMemoize.mdx b/website/docs/api/weakMapMemoize.mdx index 088f4d021..f854c5cdd 100644 --- a/website/docs/api/weakMapMemoize.mdx +++ b/website/docs/api/weakMapMemoize.mdx @@ -41,7 +41,7 @@ useSelector(state => selectSomeData(state, id)) Prior to `weakMapMemoize`, you had this problem: - +{/* START: weakMapMemoize/cacheSizeProblem.ts */} - +{/* END: weakMapMemoize/cacheSizeProblem.ts */} Before you could solve this in a number of different ways: 1. Set the `maxSize` with : - +{/* START: weakMapMemoize/setMaxSize.ts */} - +{/* END: weakMapMemoize/setMaxSize.ts */} But this required having to know the cache size ahead of time. 2. Create unique selector instances using . - +{/* START: weakMapMemoize/withUseMemo.tsx */} { - +{/* END: weakMapMemoize/withUseMemo.tsx */} 3. Using . - +{/* START: weakMapMemoize/withUseCallback.tsx */} { - +{/* END: weakMapMemoize/withUseCallback.tsx */} 4. Use : @@ -355,7 +355,7 @@ const selectItemsByCategory = createCachedSelector( Starting in 5.0.0, you can eliminate this problem using `weakMapMemoize`. - +{/* START: weakMapMemoize/cacheSizeSolution.ts */} - +{/* END: weakMapMemoize/cacheSizeSolution.ts */} This solves the problem of having to know and set the cache size prior to creating a memoized selector. Because `weakMapMemoize` essentially provides a dynamic cache size out of the box. @@ -454,7 +454,7 @@ A memoized function with a `.clearCache()` method attached. ### Using `weakMapMemoize` with `createSelector` - +{/* START: weakMapMemoize/usingWithCreateSelector.ts */} - +{/* END: weakMapMemoize/usingWithCreateSelector.ts */} ### Using `weakMapMemoize` with `createSelectorCreator` - +{/* START: weakMapMemoize/usingWithCreateSelectorCreator.ts */} - +{/* END: weakMapMemoize/usingWithCreateSelectorCreator.ts */} diff --git a/website/docs/introduction/getting-started.mdx b/website/docs/introduction/getting-started.mdx index f3936c5f7..6f8fe1471 100644 --- a/website/docs/introduction/getting-started.mdx +++ b/website/docs/introduction/getting-started.mdx @@ -48,7 +48,7 @@ Reselect exports a `createSelector` API, which generates memoized selector funct You can play around with the following **example** in this CodeSandbox: - +{/* START: basicUsage.ts */} - +{/* END: basicUsage.ts */} As you can see from the example above, `memoizedSelectCompletedTodos` does not run the second or third time, but we still get the same return value as last time. diff --git a/website/docs/usage/handling-empty-array-results.mdx b/website/docs/usage/handling-empty-array-results.mdx index dbcd01568..b7270f1d1 100644 --- a/website/docs/usage/handling-empty-array-results.mdx +++ b/website/docs/usage/handling-empty-array-results.mdx @@ -16,7 +16,7 @@ To reduce recalculations, use a predefined empty array when `array.filter` or si So you can have a pattern like this: - +{/* START: handling-empty-array-results/firstPattern.ts */} state.todos], todos => { - +{/* END: handling-empty-array-results/firstPattern.ts */} Or to avoid repetition, you can create a wrapper function and reuse it: - +{/* START: handling-empty-array-results/fallbackToEmptyArray.ts */} state.todos], todos => { - +{/* END: handling-empty-array-results/fallbackToEmptyArray.ts */} This way if the returns an empty array twice in a row, your component will not re-render due to a stable empty array reference: diff --git a/website/insertCodeExamples.ts b/website/insertCodeExamples.ts index 13859b850..bdbce5438 100644 --- a/website/insertCodeExamples.ts +++ b/website/insertCodeExamples.ts @@ -7,7 +7,8 @@ import { tsExtensionRegex } from './compileExamples' -const placeholderRegex = /([\s\S]*?)/g +const placeholderRegex = + /\{\/\* START: (.*?) \*\/\}([\s\S]*?)\{\/\* END: \1 \*\/\}/g const collectMarkdownFiles = ( directory: string, @@ -67,7 +68,7 @@ const insertCodeExamples = (examplesDirectory: string) => { const tsFileContent = readFileSync(tsFilePath, 'utf-8') const jsFileContent = readFileSync(jsFilePath, 'utf-8') - return ` + return `{/* START: ${tsFileName} */} { - ` +{/* END: ${tsFileName} */}` } ) writeFileSync(markdownFilePath, content)