From 42caf87e0e1ca47864a6535ae993a68ac150c13b Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 2 Nov 2025 18:18:28 -0500 Subject: [PATCH 1/8] Add filterMap util --- packages/toolkit/src/query/utils/filterMap.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 packages/toolkit/src/query/utils/filterMap.ts diff --git a/packages/toolkit/src/query/utils/filterMap.ts b/packages/toolkit/src/query/utils/filterMap.ts new file mode 100644 index 0000000000..ba68411dc0 --- /dev/null +++ b/packages/toolkit/src/query/utils/filterMap.ts @@ -0,0 +1,32 @@ +// Preserve type guard predicate behavior when passing to mapper +export function filterMap( + array: readonly T[], + predicate: (item: T, index: number) => item is S, + mapper: (item: S, index: number) => U | U[], +): U[] + +export function filterMap( + array: readonly T[], + predicate: (item: T, index: number) => boolean, + mapper: (item: T, index: number) => U | U[], +): U[] + +export function filterMap( + array: readonly T[], + predicate: (item: T, index: number) => boolean, + mapper: (item: T, index: number) => U | U[], +): U[] { + const result: U[] = [] + for (let i = 0; i < array.length; i++) { + const item = array[i] + if (predicate(item, i)) { + const mapped = mapper(item, i) + if (Array.isArray(mapped)) { + result.push(...mapped) + } else { + result.push(mapped) + } + } + } + return result +} From d6935de9810dd55768753af9d4c27e55e5433d51 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 2 Nov 2025 18:19:59 -0500 Subject: [PATCH 2/8] Use filterMap instead of multiple filters --- .../toolkit/src/query/core/buildSelectors.ts | 55 +++++++++---------- .../toolkit/src/query/endpointDefinitions.ts | 26 +++++---- packages/toolkit/src/query/utils/flatten.ts | 6 -- packages/toolkit/src/query/utils/index.ts | 2 +- 4 files changed, 41 insertions(+), 48 deletions(-) delete mode 100644 packages/toolkit/src/query/utils/flatten.ts diff --git a/packages/toolkit/src/query/core/buildSelectors.ts b/packages/toolkit/src/query/core/buildSelectors.ts index c6ff37191b..fd6e1f77b5 100644 --- a/packages/toolkit/src/query/core/buildSelectors.ts +++ b/packages/toolkit/src/query/core/buildSelectors.ts @@ -13,7 +13,7 @@ import type { TagTypesFrom, } from '../endpointDefinitions' import { expandTagDescription } from '../endpointDefinitions' -import { flatten, isNotNullish } from '../utils' +import { filterMap, isNotNullish } from '../utils' import type { InfiniteData, InfiniteQueryConfigOptions, @@ -342,7 +342,8 @@ export function buildSelectors< }> { const apiState = state[reducerPath] const toInvalidate = new Set() - for (const tag of tags.filter(isNotNullish).map(expandTagDescription)) { + const finalTags = filterMap(tags, isNotNullish, expandTagDescription) + for (const tag of finalTags) { const provided = apiState.provided.tags[tag.type] if (!provided) { continue @@ -353,27 +354,23 @@ export function buildSelectors< ? // id given: invalidate all queries that provide this type & id provided[tag.id] : // no id: invalidate all queries that provide this type - flatten(Object.values(provided))) ?? [] + Object.values(provided).flat()) ?? [] for (const invalidate of invalidateSubscriptions) { toInvalidate.add(invalidate) } } - return flatten( - Array.from(toInvalidate.values()).map((queryCacheKey) => { - const querySubState = apiState.queries[queryCacheKey] - return querySubState - ? [ - { - queryCacheKey, - endpointName: querySubState.endpointName!, - originalArgs: querySubState.originalArgs, - }, - ] - : [] - }), - ) + return Array.from(toInvalidate.values()).flatMap((queryCacheKey) => { + const querySubState = apiState.queries[queryCacheKey] + return querySubState + ? { + queryCacheKey, + endpointName: querySubState.endpointName!, + originalArgs: querySubState.originalArgs, + } + : [] + }) } function selectCachedArgsForQuery< @@ -382,18 +379,18 @@ export function buildSelectors< state: RootState, queryName: QueryName, ): Array> { - return Object.values(selectQueries(state) as QueryState) - .filter( - ( - entry, - ): entry is Exclude< - QuerySubState, - { status: QueryStatus.uninitialized } - > => - entry?.endpointName === queryName && - entry.status !== QueryStatus.uninitialized, - ) - .map((entry) => entry.originalArgs) + return filterMap( + Object.values(selectQueries(state) as QueryState), + ( + entry, + ): entry is Exclude< + QuerySubState, + { status: QueryStatus.uninitialized } + > => + entry?.endpointName === queryName && + entry.status !== QueryStatus.uninitialized, + (entry) => entry.originalArgs, + ) } function getHasNextPage( diff --git a/packages/toolkit/src/query/endpointDefinitions.ts b/packages/toolkit/src/query/endpointDefinitions.ts index 9b092c9a5a..19900dba80 100644 --- a/packages/toolkit/src/query/endpointDefinitions.ts +++ b/packages/toolkit/src/query/endpointDefinitions.ts @@ -39,6 +39,7 @@ import type { } from './tsHelpers' import { isNotNullish } from './utils' import type { NamedSchemaError } from './standardSchema' +import { filterMap } from './utils/filterMap' const rawResultType = /* @__PURE__ */ Symbol() const resultType = /* @__PURE__ */ Symbol() @@ -1406,20 +1407,21 @@ export function calculateProvidedBy( meta: MetaType | undefined, assertTagTypes: AssertTagTypes, ): readonly FullTagDescription[] { - if (isFunction(description)) { - return description( - result as ResultType, - error as undefined, - queryArg, - meta as MetaType, + const finalDescription = isFunction(description) + ? description( + result as ResultType, + error as undefined, + queryArg, + meta as MetaType, + ) + : description + + if (finalDescription) { + return filterMap(finalDescription, isNotNullish, (tag) => + assertTagTypes(expandTagDescription(tag)), ) - .filter(isNotNullish) - .map(expandTagDescription) - .map(assertTagTypes) - } - if (Array.isArray(description)) { - return description.map(expandTagDescription).map(assertTagTypes) } + return [] } diff --git a/packages/toolkit/src/query/utils/flatten.ts b/packages/toolkit/src/query/utils/flatten.ts deleted file mode 100644 index 253cf68fa9..0000000000 --- a/packages/toolkit/src/query/utils/flatten.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Alternative to `Array.flat(1)` - * @param arr An array like [1,2,3,[1,2]] - * @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat - */ -export const flatten = (arr: readonly any[]) => [].concat(...arr) diff --git a/packages/toolkit/src/query/utils/index.ts b/packages/toolkit/src/query/utils/index.ts index 916b32fd60..6abcf9914f 100644 --- a/packages/toolkit/src/query/utils/index.ts +++ b/packages/toolkit/src/query/utils/index.ts @@ -1,7 +1,7 @@ export * from './capitalize' export * from './copyWithStructuralSharing' export * from './countObjectKeys' -export * from './flatten' +export * from './filterMap' export * from './isAbsoluteUrl' export * from './isDocumentVisible' export * from './isNotNullish' From d633b77a9b81e0fe368ec277691931fe6b13cd9a Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 2 Nov 2025 18:20:09 -0500 Subject: [PATCH 3/8] Copy getCurrent util --- packages/toolkit/src/query/utils/getCurrent.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 packages/toolkit/src/query/utils/getCurrent.ts diff --git a/packages/toolkit/src/query/utils/getCurrent.ts b/packages/toolkit/src/query/utils/getCurrent.ts new file mode 100644 index 0000000000..058ee46a69 --- /dev/null +++ b/packages/toolkit/src/query/utils/getCurrent.ts @@ -0,0 +1,6 @@ +import type { Draft } from 'immer' +import { current, isDraft } from 'immer' + +export function getCurrent(value: T | Draft): T { + return (isDraft(value) ? current(value) : value) as T +} From a972bb355df4469d04ac516eb16a6247a4ad9d36 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 2 Nov 2025 18:21:07 -0500 Subject: [PATCH 4/8] Iterate over current arrays where possible --- .../src/query/core/buildMiddleware/cacheLifecycle.ts | 7 +++++-- packages/toolkit/src/query/core/buildSlice.ts | 5 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts b/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts index 86a7156c8c..864c547c4f 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts @@ -4,8 +4,11 @@ import type { BaseQueryMeta, BaseQueryResult, } from '../../baseQueryTypes' -import type { BaseEndpointDefinition } from '../../endpointDefinitions' -import { DefinitionType, isAnyQueryDefinition } from '../../endpointDefinitions' +import type { + BaseEndpointDefinition, + DefinitionType, +} from '../../endpointDefinitions' +import { isAnyQueryDefinition } from '../../endpointDefinitions' import type { QueryCacheKey, RootState } from '../apiState' import type { MutationResultSelectorResult, diff --git a/packages/toolkit/src/query/core/buildSlice.ts b/packages/toolkit/src/query/core/buildSlice.ts index d9f720c33a..ceb64fac41 100644 --- a/packages/toolkit/src/query/core/buildSlice.ts +++ b/packages/toolkit/src/query/core/buildSlice.ts @@ -57,6 +57,7 @@ import type { ApiContext } from '../apiTypes' import { isUpsertQuery } from './buildInitiate' import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs' import type { UnwrapPromise } from '../tsHelpers' +import { getCurrent } from '../utils/getCurrent' /** * A typesafe single entry to be upserted into the cache @@ -587,7 +588,7 @@ export function buildSlice({ draft: InvalidationState, queryCacheKey: QueryCacheKey, ) { - const existingTags = draft.keys[queryCacheKey] ?? [] + const existingTags = getCurrent(draft.keys[queryCacheKey] ?? []) // Delete this cache key from any existing tags that may have provided it for (const tag of existingTags) { @@ -596,7 +597,7 @@ export function buildSlice({ const tagSubscriptions = draft.tags[tagType]?.[tagId] if (tagSubscriptions) { - draft.tags[tagType][tagId] = tagSubscriptions.filter( + draft.tags[tagType][tagId] = getCurrent(tagSubscriptions).filter( (qc) => qc !== queryCacheKey, ) } From 20be2c5398d0697f8dd2c45f89227de73ac9b0ba Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 2 Nov 2025 18:22:39 -0500 Subject: [PATCH 5/8] Track pending requests counter --- .../buildMiddleware/invalidationByTags.ts | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/toolkit/src/query/core/buildMiddleware/invalidationByTags.ts b/packages/toolkit/src/query/core/buildMiddleware/invalidationByTags.ts index ae50030894..d00e7ee7d7 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/invalidationByTags.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/invalidationByTags.ts @@ -38,13 +38,25 @@ export const buildInvalidationByTagsHandler: InternalHandlerBuilder = ({ ) const isQueryEnd = isAnyOf( - isFulfilled(mutationThunk, queryThunk), - isRejected(mutationThunk, queryThunk), + isFulfilled(queryThunk, mutationThunk), + isRejected(queryThunk, mutationThunk), ) - let pendingTagInvalidations: FullTagDescription[] = [] + // Track via counter so we can avoid iterating over state every time + let pendingRequestCount = 0 const handler: ApiMiddlewareInternalHandler = (action, mwApi) => { + if ( + queryThunk.pending.match(action) || + mutationThunk.pending.match(action) + ) { + pendingRequestCount++ + } + + if (isQueryEnd(action)) { + pendingRequestCount = Math.max(0, pendingRequestCount - 1) + } + if (isThunkActionWithTags(action)) { invalidateTags( calculateProvidedByThunk( @@ -72,16 +84,8 @@ export const buildInvalidationByTagsHandler: InternalHandlerBuilder = ({ } } - function hasPendingRequests( - state: CombinedState, - ) { - const { queries, mutations } = state - for (const cacheRecord of [queries, mutations]) { - for (const key in cacheRecord) { - if (cacheRecord[key]?.status === QueryStatus.pending) return true - } - } - return false + function hasPendingRequests() { + return pendingRequestCount > 0 } function invalidateTags( @@ -95,7 +99,7 @@ export const buildInvalidationByTagsHandler: InternalHandlerBuilder = ({ if ( state.config.invalidationBehavior === 'delayed' && - hasPendingRequests(state) + hasPendingRequests() ) { return } From bec3efc6880ba45d842b6737879fd06149337d9e Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 2 Nov 2025 18:22:53 -0500 Subject: [PATCH 6/8] Fix typo in build config name --- packages/toolkit/tsup.config.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolkit/tsup.config.mts b/packages/toolkit/tsup.config.mts index 1222e0634d..5bf9d611e3 100644 --- a/packages/toolkit/tsup.config.mts +++ b/packages/toolkit/tsup.config.mts @@ -242,7 +242,7 @@ export default defineConfig((overrideOptions): TsupOptions[] => { }, { ...commonOptions, - name: 'Redux-Toolkit-Nexted-Legacy-ESM', + name: 'Redux-Toolkit-Nested-Legacy-ESM', external: commonOptions.external.concat('@reduxjs/toolkit'), entry: { 'react/redux-toolkit-react.legacy-esm': 'src/react/index.ts', From 6f34ce33e0ed26b6dbe9e73fba21f7738380662c Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 2 Nov 2025 18:37:34 -0500 Subject: [PATCH 7/8] Remove flatten tests --- packages/toolkit/src/query/tests/utils.test.ts | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/packages/toolkit/src/query/tests/utils.test.ts b/packages/toolkit/src/query/tests/utils.test.ts index 859865c6d7..0ed1e1fd3a 100644 --- a/packages/toolkit/src/query/tests/utils.test.ts +++ b/packages/toolkit/src/query/tests/utils.test.ts @@ -1,10 +1,5 @@ import { vi } from 'vitest' -import { - isOnline, - isDocumentVisible, - flatten, - joinUrls, -} from '@internal/query/utils' +import { isOnline, isDocumentVisible, joinUrls } from '@internal/query/utils' afterAll(() => { vi.restoreAllMocks() @@ -96,14 +91,3 @@ describe('joinUrls', () => { expect(joinUrls(base, url)).toBe(expected) }) }) - -describe('flatten', () => { - test('flattens an array to a depth of 1', () => { - expect(flatten([1, 2, [3, 4]])).toEqual([1, 2, 3, 4]) - }) - test('does not flatten to a depth of 2', () => { - const flattenResult = flatten([1, 2, [3, 4, [5, 6]]]) - expect(flattenResult).not.toEqual([1, 2, 3, 4, 5, 6]) - expect(flattenResult).toEqual([1, 2, 3, 4, [5, 6]]) - }) -}) From d02a7ddcf225c70675897ed45c933bb93175f3ba Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 2 Nov 2025 18:46:13 -0500 Subject: [PATCH 8/8] Use reduce+flat for smaller filterMap size --- packages/toolkit/src/query/utils/filterMap.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/toolkit/src/query/utils/filterMap.ts b/packages/toolkit/src/query/utils/filterMap.ts index ba68411dc0..bcf1e66ed2 100644 --- a/packages/toolkit/src/query/utils/filterMap.ts +++ b/packages/toolkit/src/query/utils/filterMap.ts @@ -16,17 +16,12 @@ export function filterMap( predicate: (item: T, index: number) => boolean, mapper: (item: T, index: number) => U | U[], ): U[] { - const result: U[] = [] - for (let i = 0; i < array.length; i++) { - const item = array[i] - if (predicate(item, i)) { - const mapped = mapper(item, i) - if (Array.isArray(mapped)) { - result.push(...mapped) - } else { - result.push(mapped) + return array + .reduce<(U | U[])[]>((acc, item, i) => { + if (predicate(item as any, i)) { + acc.push(mapper(item as any, i)) } - } - } - return result + return acc + }, []) + .flat() as U[] }