diff --git a/docs/rtk-query/api/created-api/api-slice-utils.mdx b/docs/rtk-query/api/created-api/api-slice-utils.mdx index 888d916c61..6042762679 100644 --- a/docs/rtk-query/api/created-api/api-slice-utils.mdx +++ b/docs/rtk-query/api/created-api/api-slice-utils.mdx @@ -249,53 +249,59 @@ Note that [hooks](./hooks.mdx) also track state in local component state and mig dispatch(api.util.resetApiState()) ``` -## `getRunningOperationPromises` +## `getRunningQueriesThunk` and `getRunningMutationsThunk` #### Signature ```ts no-transpile -getRunningOperationPromises: () => Array> +getRunningQueriesThunk(): ThunkWithReturnValue>> +getRunningMutationsThunk(): ThunkWithReturnValue>> ``` #### Description -A function that returns all promises for running queries and mutations. +Thunks that (if dispatched) return either all running queries or mutations. +These returned values can be awaited like promises. -This is useful for SSR scenarios to await everything triggered in any way, including via hook calls, +This is useful for SSR scenarios to await all queries (or mutations) triggered in any way, including via hook calls or manually dispatching `initiate` actions. -```ts no-transpile title="Awaiting all currently running queries & mutations example" -await Promise.all(api.util.getRunningOperationPromises()) +```ts no-transpile title="Awaiting all currently running queries example" +await Promise.all(dispatch(api.util.getRunningQueriesThunk())) ``` -## `getRunningOperationPromise` +## `getRunningQueryThunk` and `getRunningMutationThunk` #### Signature ```ts no-transpile -getRunningOperationPromise: >( +getRunningQueryThunk>( endpointName: EndpointName, args: QueryArgFrom -) => - | QueryActionCreatorResult +): ThunkWithReturnValue< + | QueryActionCreatorResult< + Definitions[EndpointName] & { type: 'query' } + > | undefined +> -getRunningOperationPromise: >( +getRunningMutationThunk>( endpointName: EndpointName, fixedCacheKeyOrRequestId: string -) => - | MutationActionCreatorResult +): ThunkWithReturnValue< + | MutationActionCreatorResult< + Definitions[EndpointName] & { type: 'mutation' } + > | undefined +> ``` #### Description -A function that returns a single promise for a given endpoint name + argument combination, -if it is currently running. If it is not currently running, the function returns `undefined`. +Thunks that (if dispatched) return a single running query (or mutation) for a given +endpoint name + argument (or requestId/fixedCacheKey) combination, if it is currently running. +If it is not currently running, the function returns `undefined`. -When used with mutation endpoints, it accepts a [fixed cache key](./hooks.mdx#signature-1) -or request ID rather than the argument. - -This is primarily added to add experimental support for suspense in the future. -It enables writing custom hooks that look up if RTK Query has already got a running promise -for a certain endpoint/argument combination, and retrieving that promise to `throw` it. +These thunks are primarily added to add experimental support for suspense in the future. +They enable writing custom hooks that look up if RTK Query has already got a running query/mutation +for a certain endpoint/argument combination, and retrieving that to `throw` it as a promise. diff --git a/docs/rtk-query/api/created-api/overview.mdx b/docs/rtk-query/api/created-api/overview.mdx index 2ce19fe0d4..c64dd64b72 100644 --- a/docs/rtk-query/api/created-api/overview.mdx +++ b/docs/rtk-query/api/created-api/overview.mdx @@ -53,20 +53,29 @@ type Api = { Array>, string > - resetApiState: SliceActions['resetApiState'] - getRunningOperationPromises: () => Array> - getRunningOperationPromise: >( + selectInvalidatedBy: ( + state: FullState, + tags: Array> + ) => Array<{ + endpointName: string + originalArgs: any + queryCacheKey: string + }> + resetApiState: ActionCreator + getRunningQueryThunk( endpointName: EndpointName, - args: QueryArgFrom - ) => - | QueryActionCreatorResult - | undefined - getRunningOperationPromise: >( + args: QueryArg + ): ThunkWithReturnValue + getRunningMutationThunk( endpointName: EndpointName, fixedCacheKeyOrRequestId: string - ) => - | MutationActionCreatorResult - | undefined + ): ThunkWithReturnValue + getRunningQueriesThunk(): ThunkWithReturnValue< + Array> + > + getRunningMutationsThunk(): ThunkWithReturnValue< + Array> + > } // Internal actions diff --git a/docs/rtk-query/usage/server-side-rendering.mdx b/docs/rtk-query/usage/server-side-rendering.mdx index 94ee03403e..4f5da85db2 100644 --- a/docs/rtk-query/usage/server-side-rendering.mdx +++ b/docs/rtk-query/usage/server-side-rendering.mdx @@ -21,7 +21,7 @@ The workflow is as follows: - Set up `next-redux-wrapper` - In `getStaticProps` or `getServerSideProps`: - Pre-fetch all queries via the `initiate` actions, e.g. `store.dispatch(api.endpoints.getPokemonByName.initiate(name))` - - Wait for each query to finish using `await Promise.all(api.util.getRunningOperationPromises())` + - Wait for each query to finish using `await Promise.all(dispatch(api.util.getRunningQueriesThunk()))` - In your `createApi` call, configure rehydration using the `extractRehydrationInfo` option: [examples](docblock://query/createApi.ts?token=CreateApiOptions.extractRehydrationInfo) @@ -56,4 +56,4 @@ The workflow is as follows: [examples](docblock://query/react/module.ts?token=ReactHooksModuleOptions.unstable__sideEffectsInRender) - Use your custom `createApi` when calling `const api = createApi({...})` -- Wait for all queries to finish using `await Promise.all(api.util.getRunningOperationPromises())` before performing the next render cycle +- Wait for all queries to finish using `await Promise.all(dispatch(api.util.getRunningQueriesThunk()))` before performing the next render cycle diff --git a/packages/toolkit/src/query/core/buildInitiate.ts b/packages/toolkit/src/query/core/buildInitiate.ts index f29514cbdc..c4f6d55371 100644 --- a/packages/toolkit/src/query/core/buildInitiate.ts +++ b/packages/toolkit/src/query/core/buildInitiate.ts @@ -14,6 +14,8 @@ import type { Api, ApiContext } from '../apiTypes' import type { ApiEndpointQuery } from './module' import type { BaseQueryError, QueryReturnValue } from '../baseQueryTypes' import type { QueryResultSelectorResult } from './buildSelectors' +import type { Dispatch } from 'redux' +import { isNotNullish } from '../utils/isNotNullish' declare module './module' { export interface ApiEndpointQuery< @@ -196,14 +198,14 @@ export function buildInitiate({ api: Api context: ApiContext }) { - const runningQueries: Record< - string, - QueryActionCreatorResult | undefined - > = {} - const runningMutations: Record< - string, - MutationActionCreatorResult | undefined - > = {} + const runningQueries: Map< + Dispatch, + Record | undefined> + > = new Map() + const runningMutations: Map< + Dispatch, + Record | undefined> + > = new Map() const { unsubscribeQueryResult, @@ -213,32 +215,80 @@ export function buildInitiate({ return { buildInitiateQuery, buildInitiateMutation, + getRunningQueryThunk, + getRunningMutationThunk, + getRunningQueriesThunk, + getRunningMutationsThunk, getRunningOperationPromises, - getRunningOperationPromise, + removalWarning, } - function getRunningOperationPromise( - endpointName: string, - argOrRequestId: any - ): any { - const endpointDefinition = context.endpointDefinitions[endpointName] - if (endpointDefinition.type === DefinitionType.query) { + /** @deprecated to be removed in 2.0 */ + function removalWarning(): never { + throw new Error( + `This method had to be removed due to a conceptual bug in RTK. + Please see https://github.com/reduxjs/redux-toolkit/pull/2481 for details. + See https://redux-toolkit.js.org/rtk-query/usage/server-side-rendering for new guidance on SSR.` + ) + } + + /** @deprecated to be removed in 2.0 */ + function getRunningOperationPromises() { + if ( + typeof process !== 'undefined' && + process.env.NODE_ENV === 'development' + ) { + removalWarning() + } else { + const extract = ( + v: Map, Record> + ) => + Array.from(v.values()).flatMap((queriesForStore) => + queriesForStore ? Object.values(queriesForStore) : [] + ) + return [...extract(runningQueries), ...extract(runningMutations)].filter( + isNotNullish + ) + } + } + + function getRunningQueryThunk(endpointName: string, queryArgs: any) { + return (dispatch: Dispatch) => { + const endpointDefinition = context.endpointDefinitions[endpointName] const queryCacheKey = serializeQueryArgs({ - queryArgs: argOrRequestId, + queryArgs, endpointDefinition, endpointName, }) - return runningQueries[queryCacheKey] - } else { - return runningMutations[argOrRequestId] + return runningQueries.get(dispatch)?.[queryCacheKey] as + | QueryActionCreatorResult + | undefined } } - function getRunningOperationPromises() { - return [ - ...Object.values(runningQueries), - ...Object.values(runningMutations), - ].filter((t: T | undefined): t is T => !!t) + function getRunningMutationThunk( + /** + * this is only here to allow TS to infer the result type by input value + * we could use it to validate the result, but it's probably not necessary + */ + _endpointName: string, + fixedCacheKeyOrRequestId: string + ) { + return (dispatch: Dispatch) => { + return runningMutations.get(dispatch)?.[fixedCacheKeyOrRequestId] as + | MutationActionCreatorResult + | undefined + } + } + + function getRunningQueriesThunk() { + return (dispatch: Dispatch) => + Object.values(runningQueries.get(dispatch) || {}).filter(isNotNullish) + } + + function getRunningMutationsThunk() { + return (dispatch: Dispatch) => + Object.values(runningMutations.get(dispatch) || {}).filter(isNotNullish) } function middlewareWarning(getState: () => RootState<{}, string, string>) { @@ -302,7 +352,7 @@ Features like automatic cache collection, automatic refetching etc. will not be const skippedSynchronously = stateAfter.requestId !== requestId - const runningQuery = runningQueries[queryCacheKey] + const runningQuery = runningQueries.get(dispatch)?.[queryCacheKey] const selectFromState = () => selector(getState()) const statePromise: QueryActionCreatorResult = Object.assign( @@ -360,9 +410,15 @@ Features like automatic cache collection, automatic refetching etc. will not be ) if (!runningQuery && !skippedSynchronously && !forceQueryFn) { - runningQueries[queryCacheKey] = statePromise + const running = runningQueries.get(dispatch) || {} + running[queryCacheKey] = statePromise + runningQueries.set(dispatch, running) + statePromise.then(() => { - delete runningQueries[queryCacheKey] + delete running[queryCacheKey] + if (!Object.keys(running).length) { + runningQueries.delete(dispatch) + } }) } @@ -404,15 +460,24 @@ Features like automatic cache collection, automatic refetching etc. will not be reset, }) - runningMutations[requestId] = ret + const running = runningMutations.get(dispatch) || {} + runningMutations.set(dispatch, running) + running[requestId] = ret ret.then(() => { - delete runningMutations[requestId] + delete running[requestId] + if (!Object.keys(running).length) { + runningMutations.delete(dispatch) + } }) if (fixedCacheKey) { - runningMutations[fixedCacheKey] = ret + running[fixedCacheKey] = ret ret.then(() => { - if (runningMutations[fixedCacheKey] === ret) - delete runningMutations[fixedCacheKey] + if (running[fixedCacheKey] === ret) { + delete running[fixedCacheKey] + if (!Object.keys(running).length) { + runningMutations.delete(dispatch) + } + } }) } diff --git a/packages/toolkit/src/query/core/module.ts b/packages/toolkit/src/query/core/module.ts index 94b0ac3be5..9950f31493 100644 --- a/packages/toolkit/src/query/core/module.ts +++ b/packages/toolkit/src/query/core/module.ts @@ -71,6 +71,9 @@ export type CoreModule = | ReferenceQueryLifecycle | ReferenceCacheCollection +interface ThunkWithReturnValue + extends ThunkAction {} + declare module '../apiTypes' { export interface ApiModules< // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -138,35 +141,90 @@ declare module '../apiTypes' { */ util: { /** - * Returns all promises for running queries and mutations. - * Useful for SSR scenarios to await everything triggered in any way, - * including via hook calls, or manually dispatching `initiate` actions. + * This method had to be removed due to a conceptual bug in RTK. + * + * Despite TypeScript errors, it will continue working in the "buggy" way it did + * before in production builds and will be removed in the next major release. + * + * Nonetheless, you should immediately replace it with the new recommended approach. + * See https://redux-toolkit.js.org/rtk-query/usage/server-side-rendering for new guidance on SSR. + * + * Please see https://github.com/reduxjs/redux-toolkit/pull/2481 for details. + * @deprecated */ - getRunningOperationPromises: () => Array> + getRunningOperationPromises: never // this is now types as `never` to immediately throw TS errors on use, but still allow for a comment + /** - * If a promise is running for a given endpoint name + argument combination, - * returns that promise. Otherwise, returns `undefined`. - * Can be used to await a specific query/mutation triggered in any way, - * including via hook calls, or manually dispatching `initiate` actions. + * This method had to be removed due to a conceptual bug in RTK. + * It has been replaced by `api.util.getRunningQueryThunk` and `api.util.getRunningMutationThunk`. + * Please see https://github.com/reduxjs/redux-toolkit/pull/2481 for details. + * @deprecated */ - getRunningOperationPromise>( + getRunningOperationPromise: never // this is now types as `never` to immediately throw TS errors on use, but still allow for a comment + + /** + * A thunk that (if dispatched) will return a specific running query, identified + * by `endpointName` and `args`. + * If that query is not running, dispatching the thunk will result in `undefined`. + * + * Can be used to await a specific query triggered in any way, + * including via hook calls or manually dispatching `initiate` actions. + * + * See https://redux-toolkit.js.org/rtk-query/usage/server-side-rendering for details. + */ + getRunningQueryThunk>( endpointName: EndpointName, args: QueryArgFrom - ): + ): ThunkWithReturnValue< | QueryActionCreatorResult< Definitions[EndpointName] & { type: 'query' } > | undefined - getRunningOperationPromise< - EndpointName extends MutationKeys - >( + > + + /** + * A thunk that (if dispatched) will return a specific running mutation, identified + * by `endpointName` and `fixedCacheKey` or `requestId`. + * If that mutation is not running, dispatching the thunk will result in `undefined`. + * + * Can be used to await a specific mutation triggered in any way, + * including via hook trigger functions or manually dispatching `initiate` actions. + * + * See https://redux-toolkit.js.org/rtk-query/usage/server-side-rendering for details. + */ + getRunningMutationThunk>( endpointName: EndpointName, fixedCacheKeyOrRequestId: string - ): + ): ThunkWithReturnValue< | MutationActionCreatorResult< Definitions[EndpointName] & { type: 'mutation' } > | undefined + > + + /** + * A thunk that (if dispatched) will return all running queries. + * + * Useful for SSR scenarios to await all running queries triggered in any way, + * including via hook calls or manually dispatching `initiate` actions. + * + * See https://redux-toolkit.js.org/rtk-query/usage/server-side-rendering for details. + */ + getRunningQueriesThunk(): ThunkWithReturnValue< + Array> + > + + /** + * A thunk that (if dispatched) will return all running mutations. + * + * Useful for SSR scenarios to await all running mutations triggered in any way, + * including via hook calls or manually dispatching `initiate` actions. + * + * See https://redux-toolkit.js.org/rtk-query/usage/server-side-rendering for details. + */ + getRunningMutationsThunk(): ThunkWithReturnValue< + Array> + > /** * A Redux thunk that can be used to manually trigger pre-fetching of data. @@ -294,6 +352,11 @@ declare module '../apiTypes' { string > + /** + * A function to select all `{ endpointName, originalArgs, queryCacheKey }` combinations that would be invalidated by a specific set of tags. + * + * Can be used for mutations that want to do optimistic updates instead of invalidating a set of tags, but don't know exactly what they need to update. + */ selectInvalidatedBy: ( state: RootState, tags: ReadonlyArray> @@ -482,8 +545,12 @@ export const coreModule = (): Module => ({ const { buildInitiateQuery, buildInitiateMutation, + getRunningMutationThunk, + getRunningMutationsThunk, + getRunningQueriesThunk, + getRunningQueryThunk, getRunningOperationPromises, - getRunningOperationPromise, + removalWarning, } = buildInitiate({ queryThunk, mutationThunk, @@ -493,8 +560,12 @@ export const coreModule = (): Module => ({ }) safeAssign(api.util, { - getRunningOperationPromises, - getRunningOperationPromise, + getRunningOperationPromises: getRunningOperationPromises as any, + getRunningOperationPromise: removalWarning as any, + getRunningMutationThunk, + getRunningMutationsThunk, + getRunningQueryThunk, + getRunningQueriesThunk, }) return { diff --git a/packages/toolkit/src/query/utils/isNotNullish.ts b/packages/toolkit/src/query/utils/isNotNullish.ts new file mode 100644 index 0000000000..e2d8f4b172 --- /dev/null +++ b/packages/toolkit/src/query/utils/isNotNullish.ts @@ -0,0 +1,3 @@ +export function isNotNullish(v: T | null | undefined): v is T { + return v != null +} diff --git a/packages/toolkit/src/tests/injectableCombineReducers.example.ts b/packages/toolkit/src/tests/injectableCombineReducers.example.ts new file mode 100644 index 0000000000..85b24359c5 --- /dev/null +++ b/packages/toolkit/src/tests/injectableCombineReducers.example.ts @@ -0,0 +1,63 @@ +/* eslint-disable import/first */ +// @ts-nocheck + +// reducer.ts or whatever + +import { combineSlices } from '@reduxjs/toolkit' + +import { sliceA } from 'fileA' +import { sliceB } from 'fileB' +import { lazySliceC } from 'fileC' +import type { lazySliceD } from 'fileD' + +import { anotherReducer } from 'somewhere' + +export interface LazyLoadedSlices {} + +export const rootReducer = combineSlices(sliceA, sliceB, { + another: anotherReducer, +}).withLazyLoadedSlices() +/* + results in a return type of + { + [sliceA.name]: SliceAState, + [sliceB.name]: SliceBState, + another: AnotherState, + [lazySliceC.name]?: SliceCState, // see fileC.ts to understand why this appears here + [lazySliceD.name]?: SliceDState, // see fileD.ts to understand why this appears here + } + */ + +// fileC.ts +// "naive" approach + +import { rootReducer, RootState } from './reducer' +import { createSlice } from '@reduxjs/toolkit' + +interface SliceCState { + foo: string +} + +declare module './reducer' { + export interface LazyLoadedSlices { + [lazySliceC.name]: SliceCState + } +} + +export const lazySliceC = createSlice({ + /* ... */ +}) +/** + * Synchronously call `injectSlice` in file. + */ +rootReducer.injectSlice(lazySliceC) + +// might want to add code for HMR as well here + +// this will still error - `lazySliceC` is optional here +const naiveSelectFoo = (state: RootState) => state.lazySliceC.foo + +const selectFoo = rootReducer.withSlice(lazySliceC).selector((state) => { + // `lazySlice` is guaranteed to not be `undefined` here. + return state.lazySlice.foo +})