Skip to content

Commit 199ad89

Browse files
phryneasShrugsymsutkowski
authored
SSR & rehydration support, suspense foundations (#1277)
Co-authored-by: Lenz Weber <mail@lenzw.de> Co-authored-by: Josh Fraser <joshfraser91@gmail.com> Co-authored-by: Matt Sutkowski <msutkowski@gmail.com>
1 parent 1f1164b commit 199ad89

File tree

8 files changed

+274
-54
lines changed

8 files changed

+274
-54
lines changed

packages/toolkit/src/query/apiTypes.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@ import type {
44
EndpointDefinition,
55
ReplaceTagTypes,
66
} from './endpointDefinitions'
7-
import type { UnionToIntersection, NoInfer } from './tsHelpers'
7+
import type {
8+
UnionToIntersection,
9+
NoInfer,
10+
WithRequiredProp,
11+
} from './tsHelpers'
812
import type { CoreModule } from './core/module'
913
import type { CreateApiOptions } from './createApi'
1014
import type { BaseQueryFn } from './baseQueryTypes'
15+
import type { CombinedState } from './core/apiState'
16+
import type { AnyAction } from '@reduxjs/toolkit'
1117

1218
export interface ApiModules<
1319
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -31,8 +37,15 @@ export type Module<Name extends ModuleName> = {
3137
TagTypes extends string
3238
>(
3339
api: Api<BaseQuery, EndpointDefinitions, ReducerPath, TagTypes, ModuleName>,
34-
options: Required<
35-
CreateApiOptions<BaseQuery, Definitions, ReducerPath, TagTypes>
40+
options: WithRequiredProp<
41+
CreateApiOptions<BaseQuery, Definitions, ReducerPath, TagTypes>,
42+
| 'reducerPath'
43+
| 'serializeQueryArgs'
44+
| 'keepUnusedDataFor'
45+
| 'refetchOnMountOrArgChange'
46+
| 'refetchOnFocus'
47+
| 'refetchOnReconnect'
48+
| 'tagTypes'
3649
>,
3750
context: ApiContext<Definitions>
3851
): {
@@ -47,6 +60,10 @@ export interface ApiContext<Definitions extends EndpointDefinitions> {
4760
apiUid: string
4861
endpointDefinitions: Definitions
4962
batch(cb: () => void): void
63+
extractRehydrationInfo: (
64+
action: AnyAction
65+
) => CombinedState<any, any, any> | undefined
66+
hasRehydrationInfo: (action: AnyAction) => boolean
5067
}
5168

5269
export type Api<

packages/toolkit/src/query/core/buildInitiate.ts

Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,12 @@ import type {
55
QueryArgFrom,
66
ResultTypeFrom,
77
} from '../endpointDefinitions'
8-
import type {
9-
QueryThunkArg,
10-
MutationThunkArg,
11-
QueryThunk,
12-
MutationThunk,
13-
} from './buildThunks'
14-
import type {
15-
AnyAction,
16-
AsyncThunk,
17-
ThunkAction,
18-
SerializedError,
19-
} from '@reduxjs/toolkit'
20-
import { unwrapResult } from '@reduxjs/toolkit'
8+
import { DefinitionType } from '../endpointDefinitions'
9+
import type { QueryThunk, MutationThunk } from './buildThunks'
10+
import type { AnyAction, ThunkAction, SerializedError } from '@reduxjs/toolkit'
2111
import type { QuerySubState, SubscriptionOptions, RootState } from './apiState'
2212
import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs'
23-
import type { Api } from '../apiTypes'
13+
import type { Api, ApiContext } from '../apiTypes'
2414
import type { ApiEndpointQuery } from './module'
2515
import type { BaseQueryError } from '../baseQueryTypes'
2616

@@ -120,6 +110,7 @@ export type MutationActionCreatorResult<
120110
* A unique string generated for the request sequence
121111
*/
122112
requestId: string
113+
123114
/**
124115
* A method to cancel the mutation promise. Note that this is not intended to prevent the mutation
125116
* that was fired off from reaching the server, but only to assist in handling the response.
@@ -189,18 +180,58 @@ export function buildInitiate({
189180
queryThunk,
190181
mutationThunk,
191182
api,
183+
context,
192184
}: {
193185
serializeQueryArgs: InternalSerializeQueryArgs
194186
queryThunk: QueryThunk
195187
mutationThunk: MutationThunk
196188
api: Api<any, EndpointDefinitions, any, any>
189+
context: ApiContext<EndpointDefinitions>
197190
}) {
191+
const runningQueries: Record<
192+
string,
193+
QueryActionCreatorResult<any> | undefined
194+
> = {}
195+
const runningMutations: Record<
196+
string,
197+
MutationActionCreatorResult<any> | undefined
198+
> = {}
199+
198200
const {
199201
unsubscribeQueryResult,
200202
removeMutationResult,
201203
updateSubscriptionOptions,
202204
} = api.internalActions
203-
return { buildInitiateQuery, buildInitiateMutation }
205+
return {
206+
buildInitiateQuery,
207+
buildInitiateMutation,
208+
getRunningOperationPromises,
209+
getRunningOperationPromise,
210+
}
211+
212+
function getRunningOperationPromise(
213+
endpointName: string,
214+
argOrRequestId: any
215+
): any {
216+
const endpointDefinition = context.endpointDefinitions[endpointName]
217+
if (endpointDefinition.type === DefinitionType.query) {
218+
const queryCacheKey = serializeQueryArgs({
219+
queryArgs: argOrRequestId,
220+
endpointDefinition,
221+
endpointName,
222+
})
223+
return runningQueries[queryCacheKey]
224+
} else {
225+
return runningMutations[argOrRequestId]
226+
}
227+
}
228+
229+
function getRunningOperationPromises() {
230+
return [
231+
...Object.values(runningQueries),
232+
...Object.values(runningMutations),
233+
].filter(<T>(t: T | undefined): t is T => !!t)
234+
}
204235

205236
function middlewareWarning(getState: () => RootState<{}, string, string>) {
206237
if (process.env.NODE_ENV !== 'production') {
@@ -242,8 +273,8 @@ Features like automatic cache collection, automatic refetching etc. will not be
242273
const thunkResult = dispatch(thunk)
243274
middlewareWarning(getState)
244275
const { requestId, abort } = thunkResult
245-
const statePromise = Object.assign(
246-
thunkResult.then(() =>
276+
const statePromise: QueryActionCreatorResult<any> = Object.assign(
277+
Promise.all([runningQueries[queryCacheKey], thunkResult]).then(() =>
247278
(api.endpoints[endpointName] as ApiEndpointQuery<any, any>).select(
248279
arg
249280
)(getState())
@@ -280,14 +311,21 @@ Features like automatic cache collection, automatic refetching etc. will not be
280311
},
281312
}
282313
)
314+
315+
if (!runningQueries[queryCacheKey]) {
316+
runningQueries[queryCacheKey] = statePromise
317+
statePromise.then(() => {
318+
delete runningQueries[queryCacheKey]
319+
})
320+
}
321+
283322
return statePromise
284323
}
285324
return queryAction
286325
}
287326

288327
function buildInitiateMutation(
289-
endpointName: string,
290-
definition: MutationDefinition<any, any, any, any>
328+
endpointName: string
291329
): StartMutationActionCreator<any> {
292330
return (arg, { track = true, fixedCacheKey } = {}) =>
293331
(dispatch, getState) => {
@@ -309,14 +347,28 @@ Features like automatic cache collection, automatic refetching etc. will not be
309347
dispatch(removeMutationResult({ requestId, fixedCacheKey }))
310348
}
311349

312-
return Object.assign(returnValuePromise, {
350+
const ret = Object.assign(returnValuePromise, {
313351
arg: thunkResult.arg,
314352
requestId,
315353
abort,
316354
unwrap: thunkResult.unwrap,
317355
unsubscribe: reset,
318356
reset,
319357
})
358+
359+
runningMutations[requestId] = ret
360+
ret.then(() => {
361+
delete runningMutations[requestId]
362+
})
363+
if (fixedCacheKey) {
364+
runningMutations[fixedCacheKey] = ret
365+
ret.then(() => {
366+
if (runningMutations[fixedCacheKey] === ret)
367+
delete runningMutations[fixedCacheKey]
368+
})
369+
}
370+
371+
return ret
320372
}
321373
}
322374
}

packages/toolkit/src/query/core/buildMiddleware/cacheCollection.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { BaseQueryFn } from '../../baseQueryTypes'
22
import type { QueryDefinition } from '../../endpointDefinitions'
3-
import type { QueryCacheKey } from '../apiState'
3+
import type { ConfigState, QueryCacheKey } from '../apiState'
44
import { QuerySubstateIdentifier } from '../apiState'
55
import type {
66
QueryStateMeta,
@@ -42,15 +42,11 @@ export const build: SubMiddlewareBuilder = ({ reducerPath, api, context }) => {
4242
const state = mwApi.getState()[reducerPath]
4343
const { queryCacheKey } = action.payload
4444

45-
const endpointDefinition = context.endpointDefinitions[
46-
state.queries[queryCacheKey]?.endpointName!
47-
] as QueryDefinition<any, any, any, any>
48-
4945
handleUnsubscribe(
5046
queryCacheKey,
47+
state.queries[queryCacheKey]?.endpointName,
5148
mwApi,
52-
endpointDefinition?.keepUnusedDataFor ??
53-
state.config.keepUnusedDataFor
49+
state.config
5450
)
5551
}
5652

@@ -61,14 +57,37 @@ export const build: SubMiddlewareBuilder = ({ reducerPath, api, context }) => {
6157
}
6258
}
6359

60+
if (context.hasRehydrationInfo(action)) {
61+
const state = mwApi.getState()[reducerPath]
62+
const { queries } = context.extractRehydrationInfo(action)!
63+
for (const [queryCacheKey, queryState] of Object.entries(queries)) {
64+
// Gotcha:
65+
// If rehydrating before the endpoint has been injected,the global `keepUnusedDataFor`
66+
// will be used instead of the endpoint-specific one.
67+
handleUnsubscribe(
68+
queryCacheKey as QueryCacheKey,
69+
queryState?.endpointName,
70+
mwApi,
71+
state.config
72+
)
73+
}
74+
}
75+
6476
return result
6577
}
6678

6779
function handleUnsubscribe(
6880
queryCacheKey: QueryCacheKey,
81+
endpointName: string | undefined,
6982
api: SubMiddlewareApi,
70-
keepUnusedDataFor: number
83+
config: ConfigState<string>
7184
) {
85+
const endpointDefinition = context.endpointDefinitions[
86+
endpointName!
87+
] as QueryDefinition<any, any, any, any>
88+
const keepUnusedDataFor =
89+
endpointDefinition?.keepUnusedDataFor ?? config.keepUnusedDataFor
90+
7291
const currentTimeout = currentRemovalTimeouts[queryCacheKey]
7392
if (currentTimeout) {
7493
clearTimeout(currentTimeout)

0 commit comments

Comments
 (0)