From dfb19ba125950a78b0b5533c38693eb145ff496d Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 2 Nov 2025 22:07:28 -0500 Subject: [PATCH 01/10] Byte-shave setupListeners --- .../toolkit/src/query/core/setupListeners.ts | 93 +++++++++++++------ 1 file changed, 64 insertions(+), 29 deletions(-) diff --git a/packages/toolkit/src/query/core/setupListeners.ts b/packages/toolkit/src/query/core/setupListeners.ts index 1c52042738..331fa9510d 100644 --- a/packages/toolkit/src/query/core/setupListeners.ts +++ b/packages/toolkit/src/query/core/setupListeners.ts @@ -4,10 +4,33 @@ import type { } from '@reduxjs/toolkit' import { createAction } from './rtkImports' -export const onFocus = /* @__PURE__ */ createAction('__rtkq/focused') -export const onFocusLost = /* @__PURE__ */ createAction('__rtkq/unfocused') -export const onOnline = /* @__PURE__ */ createAction('__rtkq/online') -export const onOffline = /* @__PURE__ */ createAction('__rtkq/offline') +export const INTERNAL_PREFIX = '__rtkq/' + +const ONLINE = 'online' +const OFFLINE = 'offline' +const FOCUS = 'focus' +const FOCUSED = 'focused' +const VISIBILITYCHANGE = 'visibilitychange' + +export const onFocus = /* @__PURE__ */ createAction( + `${INTERNAL_PREFIX}${FOCUSED}`, +) +export const onFocusLost = /* @__PURE__ */ createAction( + `${INTERNAL_PREFIX}un${FOCUSED}`, +) +export const onOnline = /* @__PURE__ */ createAction( + `${INTERNAL_PREFIX}${ONLINE}`, +) +export const onOffline = /* @__PURE__ */ createAction( + `${INTERNAL_PREFIX}${OFFLINE}`, +) + +const actions = { + onFocus, + onFocusLost, + onOnline, + onOffline, +} let initialized = false @@ -40,10 +63,13 @@ export function setupListeners( ) => () => void, ) { function defaultHandler() { - const handleFocus = () => dispatch(onFocus()) - const handleFocusLost = () => dispatch(onFocusLost()) - const handleOnline = () => dispatch(onOnline()) - const handleOffline = () => dispatch(onOffline()) + const [handleFocus, handleFocusLost, handleOnline, handleOffline] = [ + onFocus, + onFocusLost, + onOnline, + onOffline, + ].map((action) => () => dispatch(action())) + const handleVisibilityChange = () => { if (window.document.visibilityState === 'visible') { handleFocus() @@ -52,33 +78,42 @@ export function setupListeners( } } + let unsubscribe = () => { + initialized = false + } + if (!initialized) { - if (typeof window !== 'undefined' && window.addEventListener) { - // Handle focus events - window.addEventListener( - 'visibilitychange', - handleVisibilityChange, - false, - ) - window.addEventListener('focus', handleFocus, false) + const WINDOW = window + if (typeof WINDOW !== 'undefined' && !!WINDOW.addEventListener) { + const handlers = { + [FOCUS]: handleFocus, + [VISIBILITYCHANGE]: handleVisibilityChange, + [ONLINE]: handleOnline, + [OFFLINE]: handleOffline, + } - // Handle connection events - window.addEventListener('online', handleOnline, false) - window.addEventListener('offline', handleOffline, false) + function updateListeners(add: boolean) { + Object.entries(handlers).forEach(([event, handler]) => { + if (add) { + WINDOW.addEventListener(event, handler, false) + } else { + WINDOW.removeEventListener(event, handler) + } + }) + } + // Handle focus events + updateListeners(true) initialized = true + + unsubscribe = () => { + updateListeners(false) + initialized = false + } } } - const unsubscribe = () => { - window.removeEventListener('focus', handleFocus) - window.removeEventListener('visibilitychange', handleVisibilityChange) - window.removeEventListener('online', handleOnline) - window.removeEventListener('offline', handleOffline) - initialized = false - } + return unsubscribe } - return customHandler - ? customHandler(dispatch, { onFocus, onFocusLost, onOffline, onOnline }) - : defaultHandler() + return customHandler ? customHandler(dispatch, actions) : defaultHandler() } From 20bc81bc05e66bb06a2b61b1e8a4adba46b8d45f Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 2 Nov 2025 22:57:31 -0500 Subject: [PATCH 02/10] Remove dead method --- .../src/query/core/buildMiddleware/polling.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/packages/toolkit/src/query/core/buildMiddleware/polling.ts b/packages/toolkit/src/query/core/buildMiddleware/polling.ts index 70f7b177d0..2d53f3228a 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/polling.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/polling.ts @@ -75,20 +75,6 @@ export const buildPollingHandler: InternalHandlerBuilder = ({ } } - function getCacheEntrySubscriptions( - queryCacheKey: QueryCacheKey, - api: SubMiddlewareApi, - ) { - const state = api.getState()[reducerPath] - const querySubState = state.queries[queryCacheKey] - const subscriptions = currentSubscriptions.get(queryCacheKey) - - if (!querySubState || querySubState.status === QueryStatus.uninitialized) - return - - return subscriptions - } - function startNextPoll( { queryCacheKey }: QuerySubstateIdentifier, api: SubMiddlewareApi, From 5bb06ed7323ed6c2e61927d3ea26a3b9da0c5430 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 2 Nov 2025 22:57:24 -0500 Subject: [PATCH 03/10] Deduplicate TS enums with string constants --- packages/toolkit/src/query/core/apiState.ts | 21 +++++++++--- .../buildMiddleware/invalidationByTags.ts | 4 +-- .../src/query/core/buildMiddleware/polling.ts | 7 ++-- .../buildMiddleware/windowEventHandling.ts | 4 +-- .../toolkit/src/query/core/buildSelectors.ts | 8 ++--- packages/toolkit/src/query/core/buildSlice.ts | 32 +++++++++++-------- .../toolkit/src/query/core/buildThunks.ts | 4 +-- .../toolkit/src/query/react/buildHooks.ts | 2 ++ 8 files changed, 51 insertions(+), 31 deletions(-) diff --git a/packages/toolkit/src/query/core/apiState.ts b/packages/toolkit/src/query/core/apiState.ts index 91cccf9ca0..3a22a542cc 100644 --- a/packages/toolkit/src/query/core/apiState.ts +++ b/packages/toolkit/src/query/core/apiState.ts @@ -65,6 +65,13 @@ export type InfiniteData = { pageParams: Array } +// NOTE: DO NOT import and use this for runtime comparisons internally, +// except in the RTKQ React package. Use the string versions just below this. +// ESBuild auto-inlines TS enums, which bloats our bundle with many repeated +// constants like "initialized": +// https://github.com/evanw/esbuild/releases/tag/v0.14.7 +// We still have to use this in the React package since we don't publicly export +// the string constants below. /** * Strings describing the query state at any given time. */ @@ -75,6 +82,12 @@ export enum QueryStatus { rejected = 'rejected', } +// Use these string constants for runtime comparisons internally +export const STATUS_UNINITIALIZED = QueryStatus.uninitialized +export const STATUS_PENDING = QueryStatus.pending +export const STATUS_FULFILLED = QueryStatus.fulfilled +export const STATUS_REJECTED = QueryStatus.rejected + export type RequestStatusFlags = | { status: QueryStatus.uninitialized @@ -108,10 +121,10 @@ export type RequestStatusFlags = export function getRequestStatusFlags(status: QueryStatus): RequestStatusFlags { return { status, - isUninitialized: status === QueryStatus.uninitialized, - isLoading: status === QueryStatus.pending, - isSuccess: status === QueryStatus.fulfilled, - isError: status === QueryStatus.rejected, + isUninitialized: status === STATUS_UNINITIALIZED, + isLoading: status === STATUS_PENDING, + isSuccess: status === STATUS_FULFILLED, + isError: status === STATUS_REJECTED, } as any } diff --git a/packages/toolkit/src/query/core/buildMiddleware/invalidationByTags.ts b/packages/toolkit/src/query/core/buildMiddleware/invalidationByTags.ts index d00e7ee7d7..c0fccd063c 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/invalidationByTags.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/invalidationByTags.ts @@ -11,7 +11,7 @@ import type { } from '../../endpointDefinitions' import { calculateProvidedBy } from '../../endpointDefinitions' import type { CombinedState, QueryCacheKey } from '../apiState' -import { QueryStatus } from '../apiState' +import { QueryStatus, STATUS_UNINITIALIZED } from '../apiState' import { calculateProvidedByThunk } from '../buildThunks' import type { SubMiddlewareApi, @@ -127,7 +127,7 @@ export const buildInvalidationByTagsHandler: InternalHandlerBuilder = ({ queryCacheKey: queryCacheKey as QueryCacheKey, }), ) - } else if (querySubState.status !== QueryStatus.uninitialized) { + } else if (querySubState.status !== STATUS_UNINITIALIZED) { mwApi.dispatch(refetchQuery(querySubState)) } } diff --git a/packages/toolkit/src/query/core/buildMiddleware/polling.ts b/packages/toolkit/src/query/core/buildMiddleware/polling.ts index 2d53f3228a..7402cd628f 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/polling.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/polling.ts @@ -4,7 +4,7 @@ import type { Subscribers, SubscribersInternal, } from '../apiState' -import { QueryStatus } from '../apiState' +import { QueryStatus, STATUS_UNINITIALIZED } from '../apiState' import type { QueryStateMeta, SubMiddlewareApi, @@ -83,8 +83,7 @@ export const buildPollingHandler: InternalHandlerBuilder = ({ const querySubState = state.queries[queryCacheKey] const subscriptions = currentSubscriptions.get(queryCacheKey) - if (!querySubState || querySubState.status === QueryStatus.uninitialized) - return + if (!querySubState || querySubState.status === STATUS_UNINITIALIZED) return const { lowestPollingInterval, skipPollingIfUnfocused } = findLowestPollingInterval(subscriptions) @@ -119,7 +118,7 @@ export const buildPollingHandler: InternalHandlerBuilder = ({ const querySubState = state.queries[queryCacheKey] const subscriptions = currentSubscriptions.get(queryCacheKey) - if (!querySubState || querySubState.status === QueryStatus.uninitialized) { + if (!querySubState || querySubState.status === STATUS_UNINITIALIZED) { return } diff --git a/packages/toolkit/src/query/core/buildMiddleware/windowEventHandling.ts b/packages/toolkit/src/query/core/buildMiddleware/windowEventHandling.ts index 53b5c87230..914d449e1e 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/windowEventHandling.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/windowEventHandling.ts @@ -1,4 +1,4 @@ -import { QueryStatus } from '../apiState' +import { QueryStatus, STATUS_UNINITIALIZED } from '../apiState' import type { QueryCacheKey } from '../apiState' import { onFocus, onOnline } from '../setupListeners' import type { @@ -53,7 +53,7 @@ export const buildWindowEventHandler: InternalHandlerBuilder = ({ queryCacheKey: queryCacheKey as QueryCacheKey, }), ) - } else if (querySubState.status !== QueryStatus.uninitialized) { + } else if (querySubState.status !== STATUS_UNINITIALIZED) { api.dispatch(refetchQuery(querySubState)) } } diff --git a/packages/toolkit/src/query/core/buildSelectors.ts b/packages/toolkit/src/query/core/buildSelectors.ts index fd6e1f77b5..58581928fc 100644 --- a/packages/toolkit/src/query/core/buildSelectors.ts +++ b/packages/toolkit/src/query/core/buildSelectors.ts @@ -20,13 +20,13 @@ import type { InfiniteQuerySubState, MutationSubState, QueryCacheKey, - QueryKeys, QueryState, QuerySubState, RequestStatusFlags, RootState as _RootState, + QueryStatus, } from './apiState' -import { QueryStatus, getRequestStatusFlags } from './apiState' +import { STATUS_UNINITIALIZED, getRequestStatusFlags } from './apiState' import { getMutationCacheKey } from './buildSlice' import type { createSelector as _createSelector } from './rtkImports' import { createNextState } from './rtkImports' @@ -151,7 +151,7 @@ export type MutationResultSelectorResult< > = MutationSubState & RequestStatusFlags const initialSubState: QuerySubState = { - status: QueryStatus.uninitialized as const, + status: STATUS_UNINITIALIZED, } // abuse immer to freeze default states @@ -388,7 +388,7 @@ export function buildSelectors< { status: QueryStatus.uninitialized } > => entry?.endpointName === queryName && - entry.status !== QueryStatus.uninitialized, + entry.status !== STATUS_UNINITIALIZED, (entry) => entry.originalArgs, ) } diff --git a/packages/toolkit/src/query/core/buildSlice.ts b/packages/toolkit/src/query/core/buildSlice.ts index ceb64fac41..e637f1e35c 100644 --- a/packages/toolkit/src/query/core/buildSlice.ts +++ b/packages/toolkit/src/query/core/buildSlice.ts @@ -26,7 +26,13 @@ import type { InfiniteQuerySubState, InfiniteQueryDirection, } from './apiState' -import { QueryStatus } from './apiState' +import { + STATUS_FULFILLED, + STATUS_PENDING, + QueryStatus, + STATUS_REJECTED, + STATUS_UNINITIALIZED, +} from './apiState' import type { AllQueryKeys, QueryArgFromAnyQueryDefinition, @@ -46,7 +52,7 @@ import { } from '../endpointDefinitions' import type { Patch } from 'immer' import { isDraft } from 'immer' -import { applyPatches, original } from 'immer' +import { applyPatches, original } from '../utils/immerImports' import { onFocus, onFocusLost, onOffline, onOnline } from './setupListeners' import { isDocumentVisible, @@ -189,12 +195,12 @@ export function buildSlice({ } & { startedTimeStamp: number }, ) { draft[arg.queryCacheKey] ??= { - status: QueryStatus.uninitialized, + status: STATUS_UNINITIALIZED, endpointName: arg.endpointName, } updateQuerySubstateIfExists(draft, arg.queryCacheKey, (substate) => { - substate.status = QueryStatus.pending + substate.status = STATUS_PENDING substate.requestId = upserting && substate.requestId @@ -233,7 +239,7 @@ export function buildSlice({ any, any > - substate.status = QueryStatus.fulfilled + substate.status = STATUS_FULFILLED if (merge) { if (substate.data !== undefined) { @@ -390,7 +396,7 @@ export function buildSlice({ } else { // request failed if (substate.requestId !== requestId) return - substate.status = QueryStatus.rejected + substate.status = STATUS_REJECTED substate.error = (payload ?? error) as any } }, @@ -402,8 +408,8 @@ export function buildSlice({ for (const [key, entry] of Object.entries(queries)) { if ( // do not rehydrate entries that were currently in flight. - entry?.status === QueryStatus.fulfilled || - entry?.status === QueryStatus.rejected + entry?.status === STATUS_FULFILLED || + entry?.status === STATUS_REJECTED ) { draft[key] = entry } @@ -434,7 +440,7 @@ export function buildSlice({ draft[getMutationCacheKey(meta)] = { requestId, - status: QueryStatus.pending, + status: STATUS_PENDING, endpointName: arg.endpointName, startedTimeStamp, } @@ -445,7 +451,7 @@ export function buildSlice({ updateMutationSubstateIfExists(draft, meta, (substate) => { if (substate.requestId !== meta.requestId) return - substate.status = QueryStatus.fulfilled + substate.status = STATUS_FULFILLED substate.data = payload substate.fulfilledTimeStamp = meta.fulfilledTimeStamp }) @@ -456,7 +462,7 @@ export function buildSlice({ updateMutationSubstateIfExists(draft, meta, (substate) => { if (substate.requestId !== meta.requestId) return - substate.status = QueryStatus.rejected + substate.status = STATUS_REJECTED substate.error = (payload ?? error) as any }) }) @@ -465,8 +471,8 @@ export function buildSlice({ for (const [key, entry] of Object.entries(mutations)) { if ( // do not rehydrate entries that were currently in flight. - (entry?.status === QueryStatus.fulfilled || - entry?.status === QueryStatus.rejected) && + (entry?.status === STATUS_FULFILLED || + entry?.status === STATUS_REJECTED) && // only rehydrate endpoints that were persisted using a `fixedCacheKey` key !== entry?.requestId ) { diff --git a/packages/toolkit/src/query/core/buildThunks.ts b/packages/toolkit/src/query/core/buildThunks.ts index 21b233a4c9..ac9c25ca71 100644 --- a/packages/toolkit/src/query/core/buildThunks.ts +++ b/packages/toolkit/src/query/core/buildThunks.ts @@ -50,7 +50,7 @@ import type { InfiniteQueryDirection, InfiniteQueryKeys, } from './apiState' -import { QueryStatus } from './apiState' +import { QueryStatus, STATUS_UNINITIALIZED } from './apiState' import type { InfiniteQueryActionCreatorResult, QueryActionCreatorResult, @@ -425,7 +425,7 @@ export function buildThunks< ), ), } - if (currentState.status === QueryStatus.uninitialized) { + if (currentState.status === STATUS_UNINITIALIZED) { return ret } let newValue diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts index 139e449ce4..6e09650d6f 100644 --- a/packages/toolkit/src/query/react/buildHooks.ts +++ b/packages/toolkit/src/query/react/buildHooks.ts @@ -1414,6 +1414,8 @@ const noPendingQueryStateSelector: QueryStateSelector = ( isUninitialized: false, isFetching: true, isLoading: selected.data !== undefined ? false : true, + // This is the one place where we still have to use `QueryStatus` as an enum, + // since it's the only reference in the React package and not in the core. status: QueryStatus.pending, } as any } From 31a2720966c9646d73c3c9b38658d2ebf13946b6 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Mon, 3 Nov 2025 00:42:59 -0500 Subject: [PATCH 04/10] Deduplicate RTKQ imports --- packages/toolkit/src/query/react/ApiProvider.tsx | 4 ++-- packages/toolkit/src/query/react/buildHooks.ts | 2 +- packages/toolkit/src/query/react/index.ts | 2 +- packages/toolkit/src/query/react/reactReduxImports.ts | 2 +- packages/toolkit/src/query/react/rtkqImports.ts | 8 ++++++++ .../toolkit/src/query/react/useSerializedStableValue.ts | 2 +- 6 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 packages/toolkit/src/query/react/rtkqImports.ts diff --git a/packages/toolkit/src/query/react/ApiProvider.tsx b/packages/toolkit/src/query/react/ApiProvider.tsx index 294811c46c..ab84ca5809 100644 --- a/packages/toolkit/src/query/react/ApiProvider.tsx +++ b/packages/toolkit/src/query/react/ApiProvider.tsx @@ -3,8 +3,8 @@ import type { Context } from 'react' import { useContext, useEffect } from './reactImports' import * as React from 'react' import type { ReactReduxContextValue } from 'react-redux' -import { Provider, ReactReduxContext } from 'react-redux' -import { setupListeners } from '@reduxjs/toolkit/query' +import { Provider, ReactReduxContext } from './reactReduxImports' +import { setupListeners } from './rtkqImports' import type { Api } from '@reduxjs/toolkit/query' /** diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts index 6e09650d6f..2bc04468be 100644 --- a/packages/toolkit/src/query/react/buildHooks.ts +++ b/packages/toolkit/src/query/react/buildHooks.ts @@ -39,7 +39,7 @@ import type { TSHelpersNoInfer, TSHelpersOverride, } from '@reduxjs/toolkit/query' -import { QueryStatus, skipToken } from '@reduxjs/toolkit/query' +import { QueryStatus, skipToken } from './rtkqImports' import type { DependencyList } from 'react' import { useCallback, diff --git a/packages/toolkit/src/query/react/index.ts b/packages/toolkit/src/query/react/index.ts index 7b96482c26..c99f4d6452 100644 --- a/packages/toolkit/src/query/react/index.ts +++ b/packages/toolkit/src/query/react/index.ts @@ -2,7 +2,7 @@ // does not have to import this into each source file it rewrites. import { formatProdErrorMessage } from '@reduxjs/toolkit' -import { buildCreateApi, coreModule } from '@reduxjs/toolkit/query' +import { buildCreateApi, coreModule } from './rtkqImports' import { reactHooksModule, reactHooksModuleName } from './module' export * from '@reduxjs/toolkit/query' diff --git a/packages/toolkit/src/query/react/reactReduxImports.ts b/packages/toolkit/src/query/react/reactReduxImports.ts index 229df78291..e905d4801f 100644 --- a/packages/toolkit/src/query/react/reactReduxImports.ts +++ b/packages/toolkit/src/query/react/reactReduxImports.ts @@ -1 +1 @@ -export { shallowEqual } from 'react-redux' +export { shallowEqual, Provider, ReactReduxContext } from 'react-redux' diff --git a/packages/toolkit/src/query/react/rtkqImports.ts b/packages/toolkit/src/query/react/rtkqImports.ts new file mode 100644 index 0000000000..67f052daa9 --- /dev/null +++ b/packages/toolkit/src/query/react/rtkqImports.ts @@ -0,0 +1,8 @@ +export { + buildCreateApi, + coreModule, + copyWithStructuralSharing, + setupListeners, + QueryStatus, + skipToken, +} from '@reduxjs/toolkit/query' diff --git a/packages/toolkit/src/query/react/useSerializedStableValue.ts b/packages/toolkit/src/query/react/useSerializedStableValue.ts index e33075ee0e..2a2ec4cd9f 100644 --- a/packages/toolkit/src/query/react/useSerializedStableValue.ts +++ b/packages/toolkit/src/query/react/useSerializedStableValue.ts @@ -1,5 +1,5 @@ import { useEffect, useRef, useMemo } from './reactImports' -import { copyWithStructuralSharing } from '@reduxjs/toolkit/query' +import { copyWithStructuralSharing } from './rtkqImports' export function useStableQueryArgs(queryArgs: T) { const cache = useRef(queryArgs) From 3d01c60a41024731d5af20b8c76543ec3b63790e Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Mon, 3 Nov 2025 00:43:27 -0500 Subject: [PATCH 05/10] Deduplicate promise unsubscribes and endpoint names --- .../toolkit/src/query/react/buildHooks.ts | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts index 2bc04468be..3289bf82c3 100644 --- a/packages/toolkit/src/query/react/buildHooks.ts +++ b/packages/toolkit/src/query/react/buildHooks.ts @@ -1474,6 +1474,15 @@ export function buildHooks({ deps?: DependencyList, ) => void = unstable__sideEffectsInRender ? (cb) => cb() : useEffect + type UnsubscribePromiseRef = React.RefObject< + { unsubscribe?: () => void } | undefined + > + + const unsubscribePromiseRef = (ref: UnsubscribePromiseRef) => + ref.current?.unsubscribe?.() + + const endpointDefinitions = context.endpointDefinitions + return { buildQueryHooks, buildInfiniteQueryHooks, @@ -1491,7 +1500,7 @@ export function buildHooks({ // in this case, reset the hook if (lastResult?.endpointName && currentState.isUninitialized) { const { endpointName } = lastResult - const endpointDefinition = context.endpointDefinitions[endpointName] + const endpointDefinition = endpointDefinitions[endpointName] if ( queryArgs !== skipToken && serializeQueryArgs({ @@ -1551,7 +1560,7 @@ export function buildHooks({ // in this case, reset the hook if (lastResult?.endpointName && currentState.isUninitialized) { const { endpointName } = lastResult - const endpointDefinition = context.endpointDefinitions[endpointName] + const endpointDefinition = endpointDefinitions[endpointName] if ( queryArgs !== skipToken && serializeQueryArgs({ @@ -1724,9 +1733,7 @@ export function buildHooks({ initiate(stableArg, { subscriptionOptions: stableSubscriptionOptions, forceRefetch: refetchOnMountOrArgChange, - ...(isInfiniteQueryDefinition( - context.endpointDefinitions[endpointName], - ) + ...(isInfiniteQueryDefinition(endpointDefinitions[endpointName]) ? { initialPageParam: stableInitialPageParam, } @@ -1832,11 +1839,11 @@ export function buildHooks({ } function usePromiseRefUnsubscribeOnUnmount( - promiseRef: React.RefObject<{ unsubscribe?: () => void } | undefined>, + promiseRef: UnsubscribePromiseRef, ) { useEffect(() => { return () => { - promiseRef.current?.unsubscribe?.() + unsubscribePromiseRef(promiseRef) // eslint-disable-next-line react-hooks/exhaustive-deps ;(promiseRef.current as any) = undefined } @@ -1924,7 +1931,7 @@ export function buildHooks({ let promise: QueryActionCreatorResult batch(() => { - promiseRef.current?.unsubscribe() + unsubscribePromiseRef(promiseRef) promiseRef.current = promise = dispatch( initiate(arg, { @@ -1954,7 +1961,7 @@ export function buildHooks({ /* cleanup on unmount */ useEffect(() => { return () => { - promiseRef?.current?.unsubscribe() + unsubscribePromiseRef(promiseRef) } }, []) @@ -2038,7 +2045,7 @@ export function buildHooks({ let promise: InfiniteQueryActionCreatorResult batch(() => { - promiseRef.current?.unsubscribe() + unsubscribePromiseRef(promiseRef) promiseRef.current = promise = dispatch( (initiate as StartInfiniteQueryActionCreator)(arg, { From 45876a2ae32edcd1e10d22328d60f9e30f2dba49 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Mon, 3 Nov 2025 00:45:14 -0500 Subject: [PATCH 06/10] Deduplicate endpoint type enum --- .../toolkit/src/query/core/buildInitiate.ts | 3 ++- .../buildMiddleware/windowEventHandling.ts | 1 - packages/toolkit/src/query/core/buildSlice.ts | 5 +++-- packages/toolkit/src/query/core/buildThunks.ts | 18 ++++++++---------- packages/toolkit/src/query/createApi.ts | 10 ++++++---- .../toolkit/src/query/endpointDefinitions.ts | 12 +++++++++--- 6 files changed, 28 insertions(+), 21 deletions(-) diff --git a/packages/toolkit/src/query/core/buildInitiate.ts b/packages/toolkit/src/query/core/buildInitiate.ts index acb6a0e734..84ac847b73 100644 --- a/packages/toolkit/src/query/core/buildInitiate.ts +++ b/packages/toolkit/src/query/core/buildInitiate.ts @@ -11,6 +11,7 @@ import type { Api, ApiContext } from '../apiTypes' import type { BaseQueryError, QueryReturnValue } from '../baseQueryTypes' import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs' import { + ENDPOINT_QUERY, isQueryDefinition, type EndpointDefinition, type EndpointDefinitions, @@ -393,7 +394,7 @@ You must add the middleware for RTK-Query to function correctly!`, const commonThunkArgs = { ...rest, - type: 'query' as const, + type: ENDPOINT_QUERY as 'query', subscribe, forceRefetch: forceRefetch, subscriptionOptions, diff --git a/packages/toolkit/src/query/core/buildMiddleware/windowEventHandling.ts b/packages/toolkit/src/query/core/buildMiddleware/windowEventHandling.ts index 914d449e1e..d641ba2b0f 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/windowEventHandling.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/windowEventHandling.ts @@ -6,7 +6,6 @@ import type { InternalHandlerBuilder, SubMiddlewareApi, } from './types' -import { countObjectKeys } from '../../utils/countObjectKeys' export const buildWindowEventHandler: InternalHandlerBuilder = ({ reducerPath, diff --git a/packages/toolkit/src/query/core/buildSlice.ts b/packages/toolkit/src/query/core/buildSlice.ts index e637f1e35c..00f7c6d266 100644 --- a/packages/toolkit/src/query/core/buildSlice.ts +++ b/packages/toolkit/src/query/core/buildSlice.ts @@ -44,6 +44,7 @@ import type { } from './buildThunks' import { calculateProvidedByThunk } from './buildThunks' import { + ENDPOINT_QUERY, isInfiniteQueryDefinition, type AssertTagTypes, type EndpointDefinitions, @@ -332,8 +333,8 @@ export function buildSlice({ const { endpointName, arg, value } = entry const endpointDefinition = definitions[endpointName] const queryDescription: QueryThunkArg = { - type: 'query', - endpointName: endpointName, + type: ENDPOINT_QUERY as 'query', + endpointName, originalArgs: entry.arg, queryCacheKey: serializeQueryArgs({ queryArgs: arg, diff --git a/packages/toolkit/src/query/core/buildThunks.ts b/packages/toolkit/src/query/core/buildThunks.ts index ac9c25ca71..c5f6df14d7 100644 --- a/packages/toolkit/src/query/core/buildThunks.ts +++ b/packages/toolkit/src/query/core/buildThunks.ts @@ -35,6 +35,7 @@ import type { } from '../endpointDefinitions' import { calculateProvidedBy, + ENDPOINT_QUERY, isInfiniteQueryDefinition, isQueryDefinition, } from '../endpointDefinitions' @@ -509,6 +510,8 @@ export function buildThunks< const { metaSchema, skipSchemaValidation = globalSkipSchemaValidation } = endpointDefinition + const isQuery = arg.type === ENDPOINT_QUERY + try { let transformResponse: TransformCallback = defaultTransformResponse @@ -520,13 +523,11 @@ export function buildThunks< extra, endpoint: arg.endpointName, type: arg.type, - forced: - arg.type === 'query' ? isForcedQuery(arg, getState()) : undefined, - queryCacheKey: arg.type === 'query' ? arg.queryCacheKey : undefined, + forced: isQuery ? isForcedQuery(arg, getState()) : undefined, + queryCacheKey: isQuery ? arg.queryCacheKey : undefined, } - const forceQueryFn = - arg.type === 'query' ? arg[forceQueryFnSymbol] : undefined + const forceQueryFn = isQuery ? arg[forceQueryFnSymbol] : undefined let finalQueryReturnValue: QueryReturnValue @@ -675,10 +676,7 @@ export function buildThunks< } } - if ( - arg.type === 'query' && - 'infiniteQueryOptions' in endpointDefinition - ) { + if (isQuery && 'infiniteQueryOptions' in endpointDefinition) { // This is an infinite query endpoint const { infiniteQueryOptions } = endpointDefinition @@ -843,7 +841,7 @@ export function buildThunks< endpoint: arg.endpointName, arg: arg.originalArgs, type: arg.type, - queryCacheKey: arg.type === 'query' ? arg.queryCacheKey : undefined, + queryCacheKey: isQuery ? arg.queryCacheKey : undefined, } endpointDefinition.onSchemaFailure?.(caughtError, info) onSchemaFailure?.(caughtError, info) diff --git a/packages/toolkit/src/query/createApi.ts b/packages/toolkit/src/query/createApi.ts index e650c64f43..ac4fcc4b9f 100644 --- a/packages/toolkit/src/query/createApi.ts +++ b/packages/toolkit/src/query/createApi.ts @@ -12,6 +12,9 @@ import type { } from './endpointDefinitions' import { DefinitionType, + ENDPOINT_INFINITEQUERY, + ENDPOINT_MUTATION, + ENDPOINT_QUERY, isInfiniteQueryDefinition, isQueryDefinition, } from './endpointDefinitions' @@ -439,10 +442,9 @@ export function buildCreateApi, ...Module[]]>( inject: Parameters[0], ) { const evaluatedEndpoints = inject.endpoints({ - query: (x) => ({ ...x, type: DefinitionType.query }) as any, - mutation: (x) => ({ ...x, type: DefinitionType.mutation }) as any, - infiniteQuery: (x) => - ({ ...x, type: DefinitionType.infinitequery }) as any, + query: (x) => ({ ...x, type: ENDPOINT_QUERY }) as any, + mutation: (x) => ({ ...x, type: ENDPOINT_MUTATION }) as any, + infiniteQuery: (x) => ({ ...x, type: ENDPOINT_INFINITEQUERY }) as any, }) for (const [endpointName, definition] of Object.entries( diff --git a/packages/toolkit/src/query/endpointDefinitions.ts b/packages/toolkit/src/query/endpointDefinitions.ts index 19900dba80..4c51326024 100644 --- a/packages/toolkit/src/query/endpointDefinitions.ts +++ b/packages/toolkit/src/query/endpointDefinitions.ts @@ -488,12 +488,18 @@ export type BaseEndpointDefinition< { extraOptions?: BaseQueryExtraOptions } > +// NOTE As with QueryStatus in `apiState.ts`, don't use this for real comparisons +// at runtime, use the string constants defined below. export enum DefinitionType { query = 'query', mutation = 'mutation', infinitequery = 'infinitequery', } +export const ENDPOINT_QUERY = DefinitionType.query +export const ENDPOINT_MUTATION = DefinitionType.mutation +export const ENDPOINT_INFINITEQUERY = DefinitionType.infinitequery + type TagDescriptionArray = ReadonlyArray< TagDescription | undefined | null > @@ -1233,19 +1239,19 @@ export type EndpointDefinitions = Record< export function isQueryDefinition( e: EndpointDefinition, ): e is QueryDefinition { - return e.type === DefinitionType.query + return e.type === ENDPOINT_QUERY } export function isMutationDefinition( e: EndpointDefinition, ): e is MutationDefinition { - return e.type === DefinitionType.mutation + return e.type === ENDPOINT_MUTATION } export function isInfiniteQueryDefinition( e: EndpointDefinition, ): e is InfiniteQueryDefinition { - return e.type === DefinitionType.infinitequery + return e.type === ENDPOINT_INFINITEQUERY } export function isAnyQueryDefinition( From 0deaefdb5785d96d608b022c544c1245daa16be7 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Mon, 3 Nov 2025 00:46:03 -0500 Subject: [PATCH 07/10] Deduplicate cache lifecycle action fields --- .../core/buildMiddleware/cacheLifecycle.ts | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts b/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts index 864c547c4f..1a2976007e 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts @@ -205,6 +205,9 @@ export const buildCacheLifecycleHandler: InternalHandlerBuilder = ({ } const lifecycleMap: Record = {} + const { removeQueryResult, removeMutationResult, cacheEntriesUpserted } = + api.internalActions + function resolveLifecycleEntry( cacheKey: string, data: unknown, @@ -229,6 +232,16 @@ export const buildCacheLifecycleHandler: InternalHandlerBuilder = ({ } } + function getActionMetaFields( + action: + | ReturnType + | ReturnType, + ) { + const { arg, requestId } = action.meta + const { endpointName, originalArgs } = arg + return [endpointName, originalArgs, requestId] as const + } + const handler: ApiMiddlewareInternalHandler = ( action, mwApi, @@ -250,13 +263,10 @@ export const buildCacheLifecycleHandler: InternalHandlerBuilder = ({ } if (queryThunk.pending.match(action)) { - checkForNewCacheKey( - action.meta.arg.endpointName, - cacheKey, - action.meta.requestId, - action.meta.arg.originalArgs, - ) - } else if (api.internalActions.cacheEntriesUpserted.match(action)) { + const [endpointName, originalArgs, requestId] = + getActionMetaFields(action) + checkForNewCacheKey(endpointName, cacheKey, requestId, originalArgs) + } else if (cacheEntriesUpserted.match(action)) { for (const { queryDescription, value } of action.payload) { const { endpointName, originalArgs, queryCacheKey } = queryDescription checkForNewCacheKey( @@ -271,19 +281,15 @@ export const buildCacheLifecycleHandler: InternalHandlerBuilder = ({ } else if (mutationThunk.pending.match(action)) { const state = mwApi.getState()[reducerPath].mutations[cacheKey] if (state) { - handleNewKey( - action.meta.arg.endpointName, - action.meta.arg.originalArgs, - cacheKey, - mwApi, - action.meta.requestId, - ) + const [endpointName, originalArgs, requestId] = + getActionMetaFields(action) + handleNewKey(endpointName, originalArgs, cacheKey, mwApi, requestId) } } else if (isFulfilledThunk(action)) { resolveLifecycleEntry(cacheKey, action.payload, action.meta.baseQueryMeta) } else if ( - api.internalActions.removeQueryResult.match(action) || - api.internalActions.removeMutationResult.match(action) + removeQueryResult.match(action) || + removeMutationResult.match(action) ) { removeLifecycleEntry(cacheKey) } else if (api.util.resetApiState.match(action)) { @@ -298,9 +304,8 @@ export const buildCacheLifecycleHandler: InternalHandlerBuilder = ({ if (isMutationThunk(action)) { return action.meta.arg.fixedCacheKey ?? action.meta.requestId } - if (api.internalActions.removeQueryResult.match(action)) - return action.payload.queryCacheKey - if (api.internalActions.removeMutationResult.match(action)) + if (removeQueryResult.match(action)) return action.payload.queryCacheKey + if (removeMutationResult.match(action)) return getMutationCacheKey(action.payload) return '' } From 4ad4523e536e188716ed2b82e6900561111116e3 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Mon, 3 Nov 2025 00:46:44 -0500 Subject: [PATCH 08/10] Deduplicate endpoint definition lookups --- packages/toolkit/src/query/apiTypes.ts | 10 +++++++++- packages/toolkit/src/query/core/buildInitiate.ts | 4 ++-- .../query/core/buildMiddleware/cacheCollection.ts | 8 +++++--- .../src/query/core/buildMiddleware/cacheLifecycle.ts | 3 ++- .../src/query/core/buildMiddleware/queryLifecycle.ts | 5 +++-- packages/toolkit/src/query/createApi.ts | 12 +++++++++--- 6 files changed, 30 insertions(+), 12 deletions(-) diff --git a/packages/toolkit/src/query/apiTypes.ts b/packages/toolkit/src/query/apiTypes.ts index 5c73e36879..7e51b99d68 100644 --- a/packages/toolkit/src/query/apiTypes.ts +++ b/packages/toolkit/src/query/apiTypes.ts @@ -1,6 +1,6 @@ import type { UnknownAction } from '@reduxjs/toolkit' import type { BaseQueryFn } from './baseQueryTypes' -import type { CombinedState, CoreModule } from './core' +import type { CombinedState, CoreModule, QueryKeys } from './core' import type { ApiModules } from './core/module' import type { CreateApiOptions } from './createApi' import type { @@ -56,6 +56,14 @@ export interface ApiContext { hasRehydrationInfo: (action: UnknownAction) => boolean } +export const getEndpointDefinition = < + Definitions extends EndpointDefinitions, + EndpointName extends keyof Definitions, +>( + context: ApiContext, + endpointName: EndpointName, +) => context.endpointDefinitions[endpointName] + export type Api< BaseQuery extends BaseQueryFn, Definitions extends EndpointDefinitions, diff --git a/packages/toolkit/src/query/core/buildInitiate.ts b/packages/toolkit/src/query/core/buildInitiate.ts index 84ac847b73..bf61a38e9c 100644 --- a/packages/toolkit/src/query/core/buildInitiate.ts +++ b/packages/toolkit/src/query/core/buildInitiate.ts @@ -7,7 +7,7 @@ import type { } from '@reduxjs/toolkit' import type { Dispatch } from 'redux' import { asSafePromise } from '../../tsHelpers' -import type { Api, ApiContext } from '../apiTypes' +import { getEndpointDefinition, type Api, type ApiContext } from '../apiTypes' import type { BaseQueryError, QueryReturnValue } from '../baseQueryTypes' import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs' import { @@ -304,7 +304,7 @@ export function buildInitiate({ function getRunningQueryThunk(endpointName: string, queryArgs: any) { return (dispatch: Dispatch) => { - const endpointDefinition = context.endpointDefinitions[endpointName] + const endpointDefinition = getEndpointDefinition(context, endpointName) const queryCacheKey = serializeQueryArgs({ queryArgs, endpointDefinition, diff --git a/packages/toolkit/src/query/core/buildMiddleware/cacheCollection.ts b/packages/toolkit/src/query/core/buildMiddleware/cacheCollection.ts index 2fa095c223..b5f9dbaef5 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/cacheCollection.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/cacheCollection.ts @@ -1,3 +1,4 @@ +import { getEndpointDefinition } from '@internal/query/apiTypes' import type { QueryDefinition } from '../../endpointDefinitions' import type { ConfigState, QueryCacheKey, QuerySubState } from '../apiState' import { isAnyOf } from '../rtkImports' @@ -155,9 +156,10 @@ export const buildCacheCollectionHandler: InternalHandlerBuilder = ({ api: SubMiddlewareApi, config: ConfigState, ) { - const endpointDefinition = context.endpointDefinitions[ - endpointName - ] as QueryDefinition + const endpointDefinition = getEndpointDefinition( + context, + endpointName, + ) as QueryDefinition const keepUnusedDataFor = endpointDefinition?.keepUnusedDataFor ?? config.keepUnusedDataFor diff --git a/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts b/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts index 1a2976007e..012d28a361 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts @@ -23,6 +23,7 @@ import type { PromiseWithKnownReason, SubMiddlewareApi, } from './types' +import { getEndpointDefinition } from '@internal/query/apiTypes' export type ReferenceCacheLifecycle = never @@ -317,7 +318,7 @@ export const buildCacheLifecycleHandler: InternalHandlerBuilder = ({ mwApi: SubMiddlewareApi, requestId: string, ) { - const endpointDefinition = context.endpointDefinitions[endpointName] + const endpointDefinition = getEndpointDefinition(context, endpointName) const onCacheEntryAdded = endpointDefinition?.onCacheEntryAdded if (!onCacheEntryAdded) return diff --git a/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts b/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts index 10d8626982..05580ca175 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts @@ -1,9 +1,10 @@ +import { getEndpointDefinition } from '@internal/query/apiTypes' import type { BaseQueryError, BaseQueryFn, BaseQueryMeta, } from '../../baseQueryTypes' -import { DefinitionType, isAnyQueryDefinition } from '../../endpointDefinitions' +import { isAnyQueryDefinition } from '../../endpointDefinitions' import type { Recipe } from '../buildThunks' import { isFulfilled, isPending, isRejected } from '../rtkImports' import type { @@ -442,7 +443,7 @@ export const buildQueryLifecycleHandler: InternalHandlerBuilder = ({ requestId, arg: { endpointName, originalArgs }, } = action.meta - const endpointDefinition = context.endpointDefinitions[endpointName] + const endpointDefinition = getEndpointDefinition(context, endpointName) const onQueryStarted = endpointDefinition?.onQueryStarted if (onQueryStarted) { const lifecycle = {} as CacheLifecycle diff --git a/packages/toolkit/src/query/createApi.ts b/packages/toolkit/src/query/createApi.ts index ac4fcc4b9f..6f33b94495 100644 --- a/packages/toolkit/src/query/createApi.ts +++ b/packages/toolkit/src/query/createApi.ts @@ -1,4 +1,10 @@ -import type { Api, ApiContext, Module, ModuleName } from './apiTypes' +import { + getEndpointDefinition, + type Api, + type ApiContext, + type Module, + type ModuleName, +} from './apiTypes' import type { CombinedState } from './core/apiState' import type { BaseQueryArg, BaseQueryFn } from './baseQueryTypes' import type { SerializeQueryArgs } from './defaultSerializeQueryArgs' @@ -421,10 +427,10 @@ export function buildCreateApi, ...Module[]]>( endpoints, )) { if (typeof partialDefinition === 'function') { - partialDefinition(context.endpointDefinitions[endpointName]) + partialDefinition(getEndpointDefinition(context, endpointName)) } else { Object.assign( - context.endpointDefinitions[endpointName] || {}, + getEndpointDefinition(context, endpointName) || {}, partialDefinition, ) } From 2775c6eb05868e598668bc2ad372658305932c96 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Mon, 3 Nov 2025 00:46:55 -0500 Subject: [PATCH 09/10] Fix missed Immer imports --- packages/toolkit/src/query/core/buildSlice.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/toolkit/src/query/core/buildSlice.ts b/packages/toolkit/src/query/core/buildSlice.ts index 00f7c6d266..fe177ad53c 100644 --- a/packages/toolkit/src/query/core/buildSlice.ts +++ b/packages/toolkit/src/query/core/buildSlice.ts @@ -52,8 +52,7 @@ import { type QueryDefinition, } from '../endpointDefinitions' import type { Patch } from 'immer' -import { isDraft } from 'immer' -import { applyPatches, original } from '../utils/immerImports' +import { applyPatches, original, isDraft } from '../utils/immerImports' import { onFocus, onFocusLost, onOffline, onOnline } from './setupListeners' import { isDocumentVisible, From 7891846ff9450b75d3be762e8a0d925a3cd99345 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Mon, 3 Nov 2025 01:04:27 -0500 Subject: [PATCH 10/10] Try size-checking browser.mjs files instead --- packages/toolkit/.size-limit.cjs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/toolkit/.size-limit.cjs b/packages/toolkit/.size-limit.cjs index a6999b566b..d82691bbc1 100644 --- a/packages/toolkit/.size-limit.cjs +++ b/packages/toolkit/.size-limit.cjs @@ -1,8 +1,11 @@ const webpack = require('webpack') let { join } = require('path') -const esmSuffixes = ['modern.mjs' /*, 'browser.mjs', 'legacy-esm.js'*/] -const cjsSuffixes = [/*'development.cjs',*/ 'production.min.cjs'] +const esmSuffixes = ['modern.mjs', 'browser.mjs' /*, 'legacy-esm.js'*/] +const cjsSuffixes = [ + /*'development.cjs',*/ + /*'production.min.cjs'*/ +] function withRtkPath(suffix, cjs = false) { /**