Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix hover preview of generated selectors. #633

Merged
merged 5 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"env": {
"browser": true,
"node": true,
"node": true
},
"extends": "eslint:recommended",
"parserOptions": {
Expand All @@ -14,7 +14,11 @@
"eol-last": 2,
"no-multiple-empty-lines": 2,
"object-curly-spacing": [2, "always"],
"quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}],
"quotes": [
2,
"single",
{ "avoidEscape": true, "allowTemplateLiterals": true }
],
"semi": [2, "never"],
"strict": 0,
"space-before-blocks": [2, "always"],
Expand Down Expand Up @@ -45,8 +49,8 @@
"@typescript-eslint/no-use-before-define": ["off"],
"@typescript-eslint/ban-types": "off",
"prefer-rest-params": "off",
"prefer-rest": "off",
"prefer-spread": "off"
"prefer-spread": "off",
"@typescript-eslint/consistent-type-imports": [2]
}
},
{
Expand All @@ -61,9 +65,6 @@
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/camelcase": "off",
"import/max-dependencies": "off",
"sonarjs/no-duplicate-string": "off",
"@typescript-eslint/no-shadow": "off"
}
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"format": "prettier --write \"{src,test}/**/*.{js,ts}\" \"docs/**/*.md\"",
"lint": "eslint src test",
"prepack": "yarn build",
"bench": "vitest --run bench",
"test": "node --expose-gc ./node_modules/vitest/dist/cli-wrapper.js run",
"test:cov": "vitest run --coverage",
"test:typescript": "tsc --noEmit -p typescript_test/tsconfig.json"
Expand Down
65 changes: 63 additions & 2 deletions src/autotrackMemoize/autotrackMemoize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,68 @@ import {
import type { AnyFunction } from '@internal/types'
import { createCache } from './autotracking'

export function autotrackMemoize<F extends AnyFunction>(func: F) {
/**
* Uses an "auto-tracking" approach inspired by the work of the Ember Glimmer team.
* It uses a Proxy to wrap arguments and track accesses to nested fields
* in your selector on first read. Later, when the selector is called with
* new arguments, it identifies which accessed fields have changed and
* only recalculates the result if one or more of those accessed fields have changed.
* This allows it to be more precise than the shallow equality checks in `defaultMemoize`.
*
* __Design Tradeoffs for `autotrackMemoize`:__
* - Pros:
* - It is likely to avoid excess calculations and recalculate fewer times than `defaultMemoize` will,
* which may also result in fewer component re-renders.
* - Cons:
* - It only has a cache size of 1.
* - It is slower than `defaultMemoize`, because it has to do more work. (How much slower is dependent on the number of accessed fields in a selector, number of calls, frequency of input changes, etc)
* - It can have some unexpected behavior. Because it tracks nested field accesses,
* cases where you don't access a field will not recalculate properly.
* For example, a badly-written selector like:
* ```ts
* createSelector([state => state.todos], todos => todos)
* ```
* that just immediately returns the extracted value will never update, because it doesn't see any field accesses to check.
*
* __Use Cases for `autotrackMemoize`:__
* - It is likely best used for cases where you need to access specific nested fields
* in data, and avoid recalculating if other fields in the same data objects are immutably updated.
*
* @param func - The function to be memoized.
* @returns A memoized function with a `.clearCache()` method attached.
*
* @example
* <caption>Using `createSelector`</caption>
* ```ts
* import { unstable_autotrackMemoize as autotrackMemoize, createSelector } from 'reselect'
*
* const selectTodoIds = createSelector(
* [(state: RootState) => state.todos],
* (todos) => todos.map(todo => todo.id),
* { memoize: autotrackMemoize }
* )
* ```
*
* @example
* <caption>Using `createSelectorCreator`</caption>
* ```ts
* import { unstable_autotrackMemoize as autotrackMemoize, createSelectorCreator } from 'reselect'
*
* const createSelectorAutotrack = createSelectorCreator(autotrackMemoize)
*
* const selectTodoIds = createSelectorAutotrack(
* [(state: RootState) => state.todos],
* (todos) => todos.map(todo => todo.id)
* )
* ```
*
* @template Func - The type of the function that is memoized.
*
* @since 5.0.0
* @public
* @experimental
*/
export function autotrackMemoize<Func extends AnyFunction>(func: Func) {
// we reference arguments instead of spreading them for performance reasons

const node: Node<Record<string, unknown>> = createNode(
Expand All @@ -34,5 +95,5 @@ export function autotrackMemoize<F extends AnyFunction>(func: F) {

memoized.clearCache = () => cache.clear()

return memoized as F & { clearCache: () => void }
return memoized as Func & { clearCache: () => void }
}
15 changes: 8 additions & 7 deletions src/autotrackMemoize/autotracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// - https://www.pzuraq.com/blog/how-autotracking-works
// - https://v5.chriskrycho.com/journal/autotracking-elegant-dx-via-cutting-edge-cs/
import type { EqualityFn } from '@internal/types'
import { assert } from './utils'
import { assertIsFunction } from '@internal/utils'

// The global revision clock. Every time state changes, the clock increments.
export let $REVISION = 0
Expand Down Expand Up @@ -133,10 +133,11 @@ export function setValue<T extends Cell<unknown>>(
storage: T,
value: CellValue<T>
): void {
assert(
storage instanceof Cell,
'setValue must be passed a tracked store created with `createStorage`.'
)
if (!(storage instanceof Cell)) {
throw new TypeError(
'setValue must be passed a tracked store created with `createStorage`.'
)
}

storage.value = storage._lastValue = value
}
Expand All @@ -149,8 +150,8 @@ export function createCell<T = unknown>(
}

export function createCache<T = unknown>(fn: () => T): TrackingCache {
assert(
typeof fn === 'function',
assertIsFunction(
fn,
'the first parameter to `createCache` must be a function'
)

Expand Down
76 changes: 50 additions & 26 deletions src/createSelectorCreator.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { OutputSelector, Selector, SelectorArray } from 'reselect'
import { defaultMemoize } from './defaultMemoize'

import type {
Expand All @@ -7,9 +8,7 @@ import type {
ExtractMemoizerFields,
GetParamsFromSelectors,
GetStateFromSelectors,
OutputSelector,
Selector,
SelectorArray,
InterruptRecursion,
StabilityCheckFrequency,
UnknownMemoizer
} from './types'
Expand All @@ -28,21 +27,25 @@ import {
*
* @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`).
* @template ArgsMemoizeFunction - The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used.
*
* @public
*/
export interface CreateSelectorFunction<
MemoizeFunction extends UnknownMemoizer,
MemoizeFunction extends UnknownMemoizer = typeof defaultMemoize,
ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize
> {
/**
* Creates a memoized selector function.
*
* @param createSelectorArgs - An arbitrary number of input selectors as separate inline arguments and a `combiner` function.
* @returns An output selector.
* @returns A memoized output selector.
*
* @template InputSelectors - The type of the input selectors as an array.
* @template Result - The return type of the `combiner` as well as the output selector.
* @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`.
* @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`.
*
* @see {@link https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-selectoroptions createSelector}
*/
<InputSelectors extends SelectorArray, Result>(
...createSelectorArgs: [
Expand All @@ -54,18 +57,21 @@ export interface CreateSelectorFunction<
Result,
MemoizeFunction,
ArgsMemoizeFunction
>
> &
InterruptRecursion

/**
* Creates a memoized selector function.
*
* @param createSelectorArgs - An arbitrary number of input selectors as separate inline arguments, a `combiner` function and an `options` object.
* @returns An output selector.
* @returns A memoized output selector.
*
* @template InputSelectors - The type of the input selectors as an array.
* @template Result - The return type of the `combiner` as well as the output selector.
* @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`.
* @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`.
*
* @see {@link https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-selectoroptions createSelector}
*/
<
InputSelectors extends SelectorArray,
Expand All @@ -90,20 +96,23 @@ export interface CreateSelectorFunction<
Result,
OverrideMemoizeFunction,
OverrideArgsMemoizeFunction
>
> &
InterruptRecursion

/**
* Creates a memoized selector function.
*
* @param inputSelectors - An array of input selectors.
* @param combiner - A function that Combines the input selectors and returns an output selector. Otherwise known as the result function.
* @param createSelectorOptions - An optional options object that allows for further customization per selector.
* @returns An output selector.
* @returns A memoized output selector.
*
* @template InputSelectors - The type of the input selectors array.
* @template Result - The return type of the `combiner` as well as the output selector.
* @template OverrideMemoizeFunction - The type of the optional `memoize` function that could be passed into the options object to override the original `memoize` function that was initially passed into `createSelectorCreator`.
* @template OverrideArgsMemoizeFunction - The type of the optional `argsMemoize` function that could be passed into the options object to override the original `argsMemoize` function that was initially passed into `createSelectorCreator`.
*
* @see {@link https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-selectoroptions createSelector}
*/
<
InputSelectors extends SelectorArray,
Expand All @@ -126,7 +135,8 @@ export interface CreateSelectorFunction<
Result,
OverrideMemoizeFunction,
OverrideArgsMemoizeFunction
>
> &
InterruptRecursion
}

let globalStabilityCheck: StabilityCheckFrequency = 'once'
Expand All @@ -149,6 +159,8 @@ let globalStabilityCheck: StabilityCheckFrequency = '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')
Expand All @@ -161,6 +173,9 @@ let globalStabilityCheck: StabilityCheckFrequency = 'once'
* ```
* @see {@link https://github.com/reduxjs/reselect#development-only-checks development-only-checks}
* @see {@link https://github.com/reduxjs/reselect#global-configuration global-configuration}
*
* @since 5.0.0
* @public
*/
export function setInputStabilityCheckEnabled(
inputStabilityCheckFrequency: StabilityCheckFrequency
Expand Down Expand Up @@ -195,15 +210,20 @@ export function setInputStabilityCheckEnabled(
*
* @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`).
* @template ArgsMemoizeFunction - The type of the optional memoize function that is used to memoize the arguments passed into the output selector generated by `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`). If none is explicitly provided, `defaultMemoize` will be used.
*
* @see {@link https://github.com/reduxjs/reselect#createselectorcreatormemoize-memoizeoptions createSelectorCreator}
*
* @since 5.0.0
* @public
*/
export function createSelectorCreator<
MemoizeFunction extends UnknownMemoizer,
ArgsMemoizeFunction extends UnknownMemoizer = typeof defaultMemoize
>(
options: CreateSelectorOptions<
MemoizeFunction,
typeof defaultMemoize,
never,
typeof defaultMemoize,
MemoizeFunction,
ArgsMemoizeFunction
>
): CreateSelectorFunction<MemoizeFunction, ArgsMemoizeFunction>
Expand All @@ -230,6 +250,10 @@ export function createSelectorCreator<
* ```
*
* @template MemoizeFunction - The type of the memoize function that is used to memoize the `resultFunc` inside `createSelector` (e.g., `defaultMemoize` or `weakMapMemoize`).
*
* @see {@link https://github.com/reduxjs/reselect#createselectorcreatormemoize-memoizeoptions createSelectorCreator}
*
* @public
*/
export function createSelectorCreator<MemoizeFunction extends UnknownMemoizer>(
memoize: MemoizeFunction,
Expand Down Expand Up @@ -280,7 +304,7 @@ export function createSelectorCreator<
OverrideMemoizeFunction extends UnknownMemoizer = MemoizeFunction,
OverrideArgsMemoizeFunction extends UnknownMemoizer = ArgsMemoizeFunction
>(
...funcs: [
...createSelectorArgs: [
...inputSelectors: [...InputSelectors],
combiner: Combiner<InputSelectors, Result>,
createSelectorOptions?: Partial<
Expand Down Expand Up @@ -309,7 +333,7 @@ export function createSelectorCreator<
> = {}

// Normally, the result func or "combiner" is the last arg
let resultFunc = funcs.pop() as
let resultFunc = createSelectorArgs.pop() as
| Combiner<InputSelectors, Result>
| Partial<
CreateSelectorOptions<
Expand All @@ -324,7 +348,7 @@ export function createSelectorCreator<
if (typeof resultFunc === 'object') {
directlyPassedOptions = resultFunc
// and pop the real result func off
resultFunc = funcs.pop() as Combiner<InputSelectors, Result>
resultFunc = createSelectorArgs.pop() as Combiner<InputSelectors, Result>
}

assertIsFunction(
Expand Down Expand Up @@ -354,7 +378,7 @@ export function createSelectorCreator<
// we wrap it in an array so we can apply it.
const finalMemoizeOptions = ensureIsArray(memoizeOptions)
const finalArgsMemoizeOptions = ensureIsArray(argsMemoizeOptions)
const dependencies = getDependencies(funcs) as InputSelectors
const dependencies = getDependencies(createSelectorArgs) as InputSelectors

const memoizedResultFunc = memoize(function recomputationWrapper() {
recomputations++
Expand All @@ -370,15 +394,6 @@ export function createSelectorCreator<
let firstRun = true

// If a selector is called with the exact same arguments we don't need to traverse our dependencies again.
// TODO This was changed to `memoize` in 4.0.0 ( #297 ), but I changed it back.
// The original intent was to allow customizing things like skortchmark's
// selector debugging setup.
// But, there's multiple issues:
// - We don't pass in `memoizeOptions`
// Arguments change all the time, but input values change less often.
// Most of the time shallow equality _is_ what we really want here.
// TODO Rethink this change, or find a way to expose more options?
// @ts-ignore
const selector = argsMemoize(function dependenciesChecker() {
/** Return values of input selectors which the `resultFunc` takes as arguments. */
const inputSelectorResults = collectInputSelectorResults(
Expand Down Expand Up @@ -410,7 +425,7 @@ export function createSelectorCreator<
lastResult = memoizedResultFunc.apply(null, inputSelectorResults)

return lastResult
}, ...finalArgsMemoizeOptions) as Selector<
}, ...finalArgsMemoizeOptions) as unknown as Selector<
GetStateFromSelectors<InputSelectors>,
Result,
GetParamsFromSelectors<InputSelectors>
Expand Down Expand Up @@ -439,5 +454,14 @@ export function createSelectorCreator<
>
}

/**
* Accepts one or more "input selectors" (either as separate arguments or a single array),
* a single "result function" / "combiner", and an optional options object, and
* generates a memoized selector function.
*
* @see {@link https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-selectoroptions createSelector}
*
* @public
*/
export const createSelector =
/* #__PURE__ */ createSelectorCreator(defaultMemoize)
Loading
Loading