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) { /** 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/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/buildInitiate.ts b/packages/toolkit/src/query/core/buildInitiate.ts index acb6a0e734..bf61a38e9c 100644 --- a/packages/toolkit/src/query/core/buildInitiate.ts +++ b/packages/toolkit/src/query/core/buildInitiate.ts @@ -7,10 +7,11 @@ 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 { + ENDPOINT_QUERY, isQueryDefinition, type EndpointDefinition, type EndpointDefinitions, @@ -303,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, @@ -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/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 864c547c4f..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 @@ -205,6 +206,9 @@ export const buildCacheLifecycleHandler: InternalHandlerBuilder = ({ } const lifecycleMap: Record = {} + const { removeQueryResult, removeMutationResult, cacheEntriesUpserted } = + api.internalActions + function resolveLifecycleEntry( cacheKey: string, data: unknown, @@ -229,6 +233,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 +264,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 +282,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 +305,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 '' } @@ -312,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/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 70f7b177d0..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, @@ -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, @@ -97,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) @@ -133,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/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/core/buildMiddleware/windowEventHandling.ts b/packages/toolkit/src/query/core/buildMiddleware/windowEventHandling.ts index 53b5c87230..d641ba2b0f 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 { @@ -6,7 +6,6 @@ import type { InternalHandlerBuilder, SubMiddlewareApi, } from './types' -import { countObjectKeys } from '../../utils/countObjectKeys' export const buildWindowEventHandler: InternalHandlerBuilder = ({ reducerPath, @@ -53,7 +52,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..fe177ad53c 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, @@ -38,6 +44,7 @@ import type { } from './buildThunks' import { calculateProvidedByThunk } from './buildThunks' import { + ENDPOINT_QUERY, isInfiniteQueryDefinition, type AssertTagTypes, type EndpointDefinitions, @@ -45,8 +52,7 @@ import { type QueryDefinition, } from '../endpointDefinitions' import type { Patch } from 'immer' -import { isDraft } from 'immer' -import { applyPatches, original } from 'immer' +import { applyPatches, original, isDraft } 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) { @@ -326,8 +332,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, @@ -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..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' @@ -50,7 +51,7 @@ import type { InfiniteQueryDirection, InfiniteQueryKeys, } from './apiState' -import { QueryStatus } from './apiState' +import { QueryStatus, STATUS_UNINITIALIZED } from './apiState' import type { InfiniteQueryActionCreatorResult, QueryActionCreatorResult, @@ -425,7 +426,7 @@ export function buildThunks< ), ), } - if (currentState.status === QueryStatus.uninitialized) { + if (currentState.status === STATUS_UNINITIALIZED) { return ret } let newValue @@ -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/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() } diff --git a/packages/toolkit/src/query/createApi.ts b/packages/toolkit/src/query/createApi.ts index e650c64f43..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' @@ -12,6 +18,9 @@ import type { } from './endpointDefinitions' import { DefinitionType, + ENDPOINT_INFINITEQUERY, + ENDPOINT_MUTATION, + ENDPOINT_QUERY, isInfiniteQueryDefinition, isQueryDefinition, } from './endpointDefinitions' @@ -418,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, ) } @@ -439,10 +448,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( 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 139e449ce4..3289bf82c3 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, @@ -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 } @@ -1472,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, @@ -1489,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({ @@ -1549,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({ @@ -1722,9 +1733,7 @@ export function buildHooks({ initiate(stableArg, { subscriptionOptions: stableSubscriptionOptions, forceRefetch: refetchOnMountOrArgChange, - ...(isInfiniteQueryDefinition( - context.endpointDefinitions[endpointName], - ) + ...(isInfiniteQueryDefinition(endpointDefinitions[endpointName]) ? { initialPageParam: stableInitialPageParam, } @@ -1830,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 } @@ -1922,7 +1931,7 @@ export function buildHooks({ let promise: QueryActionCreatorResult batch(() => { - promiseRef.current?.unsubscribe() + unsubscribePromiseRef(promiseRef) promiseRef.current = promise = dispatch( initiate(arg, { @@ -1952,7 +1961,7 @@ export function buildHooks({ /* cleanup on unmount */ useEffect(() => { return () => { - promiseRef?.current?.unsubscribe() + unsubscribePromiseRef(promiseRef) } }, []) @@ -2036,7 +2045,7 @@ export function buildHooks({ let promise: InfiniteQueryActionCreatorResult batch(() => { - promiseRef.current?.unsubscribe() + unsubscribePromiseRef(promiseRef) promiseRef.current = promise = dispatch( (initiate as StartInfiniteQueryActionCreator)(arg, { 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)