Skip to content

Commit 72e5673

Browse files
authored
Merge pull request #2266 from barnabasJ/feature/upsertQueryData
2 parents 5e4c51f + 06ee327 commit 72e5673

File tree

5 files changed

+500
-8
lines changed

5 files changed

+500
-8
lines changed

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { QueryStatus } from './apiState'
1313
import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs'
1414
import type { Api, ApiContext } from '../apiTypes'
1515
import type { ApiEndpointQuery } from './module'
16-
import type { BaseQueryError } from '../baseQueryTypes'
16+
import type { BaseQueryError, QueryReturnValue } from '../baseQueryTypes'
1717
import type { QueryResultSelectorResult } from './buildSelectors'
1818

1919
declare module './module' {
@@ -34,10 +34,13 @@ declare module './module' {
3434
}
3535
}
3636

37+
export const forceQueryFnSymbol = Symbol('forceQueryFn')
38+
3739
export interface StartQueryActionCreatorOptions {
3840
subscribe?: boolean
3941
forceRefetch?: boolean | number
4042
subscriptionOptions?: SubscriptionOptions
43+
[forceQueryFnSymbol]?: () => QueryReturnValue
4144
}
4245

4346
type StartQueryActionCreator<
@@ -259,7 +262,15 @@ Features like automatic cache collection, automatic refetching etc. will not be
259262
endpointDefinition: QueryDefinition<any, any, any, any>
260263
) {
261264
const queryAction: StartQueryActionCreator<any> =
262-
(arg, { subscribe = true, forceRefetch, subscriptionOptions } = {}) =>
265+
(
266+
arg,
267+
{
268+
subscribe = true,
269+
forceRefetch,
270+
subscriptionOptions,
271+
[forceQueryFnSymbol]: forceQueryFn,
272+
} = {}
273+
) =>
263274
(dispatch, getState) => {
264275
const queryCacheKey = serializeQueryArgs({
265276
queryArgs: arg,
@@ -274,6 +285,7 @@ Features like automatic cache collection, automatic refetching etc. will not be
274285
endpointName,
275286
originalArgs: arg,
276287
queryCacheKey,
288+
[forceQueryFnSymbol]: forceQueryFn,
277289
})
278290
const selector = (
279291
api.endpoints[endpointName] as ApiEndpointQuery<any, any>

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,14 @@ export function buildSlice({
157157
draft,
158158
meta.arg.queryCacheKey,
159159
(substate) => {
160-
if (substate.requestId !== meta.requestId) return
160+
if (substate.requestId !== meta.requestId) {
161+
if (
162+
substate.fulfilledTimeStamp &&
163+
meta.fulfilledTimeStamp < substate.fulfilledTimeStamp
164+
) {
165+
return
166+
}
167+
}
161168
const { merge } = definitions[
162169
meta.arg.endpointName
163170
] as QueryDefinition<any, any, any, any>

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

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import type {
77
} from '../baseQueryTypes'
88
import type { RootState, QueryKeys, QuerySubstateIdentifier } from './apiState'
99
import { QueryStatus } from './apiState'
10-
import type { StartQueryActionCreatorOptions } from './buildInitiate'
10+
import {
11+
forceQueryFnSymbol,
12+
StartQueryActionCreatorOptions,
13+
QueryActionCreatorResult,
14+
} from './buildInitiate'
1115
import type {
1216
AssertTagTypes,
1317
EndpointDefinition,
@@ -144,6 +148,9 @@ function defaultTransformResponse(baseQueryReturnValue: unknown) {
144148

145149
export type MaybeDrafted<T> = T | Draft<T>
146150
export type Recipe<T> = (data: MaybeDrafted<T>) => void | MaybeDrafted<T>
151+
export type UpsertRecipe<T> = (
152+
data: MaybeDrafted<T> | undefined
153+
) => void | MaybeDrafted<T>
147154

148155
export type PatchQueryDataThunk<
149156
Definitions extends EndpointDefinitions,
@@ -163,6 +170,24 @@ export type UpdateQueryDataThunk<
163170
updateRecipe: Recipe<ResultTypeFrom<Definitions[EndpointName]>>
164171
) => ThunkAction<PatchCollection, PartialState, any, AnyAction>
165172

173+
export type UpsertQueryDataThunk<
174+
Definitions extends EndpointDefinitions,
175+
PartialState
176+
> = <EndpointName extends QueryKeys<Definitions>>(
177+
endpointName: EndpointName,
178+
args: QueryArgFrom<Definitions[EndpointName]>,
179+
value: ResultTypeFrom<Definitions[EndpointName]>
180+
) => ThunkAction<
181+
QueryActionCreatorResult<
182+
Definitions[EndpointName] extends QueryDefinition<any, any, any, any>
183+
? Definitions[EndpointName]
184+
: never
185+
>,
186+
PartialState,
187+
any,
188+
AnyAction
189+
>
190+
166191
/**
167192
* An object returned from dispatching a `api.util.updateQueryData` call.
168193
*/
@@ -255,6 +280,24 @@ export function buildThunks<
255280
return ret
256281
}
257282

283+
const upsertQueryData: UpsertQueryDataThunk<Definitions, State> =
284+
(endpointName, args, value) => (dispatch) => {
285+
return dispatch(
286+
(
287+
api.endpoints[endpointName] as ApiEndpointQuery<
288+
QueryDefinition<any, any, any, any, any>,
289+
Definitions
290+
>
291+
).initiate(args, {
292+
subscribe: false,
293+
forceRefetch: true,
294+
[forceQueryFnSymbol]: () => ({
295+
data: value,
296+
}),
297+
})
298+
)
299+
}
300+
258301
const executeEndpoint: AsyncThunkPayloadCreator<
259302
ThunkResult,
260303
QueryThunkArg | MutationThunkArg,
@@ -291,7 +334,12 @@ export function buildThunks<
291334
forced:
292335
arg.type === 'query' ? isForcedQuery(arg, getState()) : undefined,
293336
}
294-
if (endpointDefinition.query) {
337+
338+
const forceQueryFn =
339+
arg.type === 'query' ? arg[forceQueryFnSymbol] : undefined
340+
if (forceQueryFn) {
341+
result = forceQueryFn()
342+
} else if (endpointDefinition.query) {
295343
result = await baseQuery(
296344
endpointDefinition.query(arg.originalArgs),
297345
baseQueryApi,
@@ -431,12 +479,17 @@ In the case of an unhandled error, no tags will be "provided" or "invalidated".`
431479
const requestState = state[reducerPath]?.queries?.[arg.queryCacheKey]
432480
const fulfilledVal = requestState?.fulfilledTimeStamp
433481

434-
// Don't retry a request that's currently in-flight
435-
if (requestState?.status === 'pending') return false
482+
// Order of these checks matters.
483+
// In order for `upsertQueryData` to successfully run while an existing request is
484+
/// in flight, we have to check `isForcedQuery` before `status === 'pending'`,
485+
// otherwise `queryThunk` will bail out and not run at all.
436486

437487
// if this is forced, continue
438488
if (isForcedQuery(arg, state)) return true
439489

490+
// Don't retry a request that's currently in-flight
491+
if (requestState?.status === 'pending') return false
492+
440493
// Pull from the cache unless we explicitly force refetch or qualify based on time
441494
if (fulfilledVal)
442495
// Value is cached and we didn't specify to refresh, skip it.
@@ -527,6 +580,7 @@ In the case of an unhandled error, no tags will be "provided" or "invalidated".`
527580
mutationThunk,
528581
prefetch,
529582
updateQueryData,
583+
upsertQueryData,
530584
patchQueryData,
531585
buildMatchThunkActions,
532586
}

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
/**
22
* Note: this file should import all other files for type discovery and declaration merging
33
*/
4-
import type { PatchQueryDataThunk, UpdateQueryDataThunk } from './buildThunks'
4+
import type {
5+
PatchQueryDataThunk,
6+
UpdateQueryDataThunk,
7+
UpsertQueryDataThunk,
8+
} from './buildThunks'
59
import { buildThunks } from './buildThunks'
610
import type {
711
ActionCreatorWithPayload,
@@ -210,6 +214,10 @@ declare module '../apiTypes' {
210214
Definitions,
211215
RootState<Definitions, string, ReducerPath>
212216
>
217+
upsertQueryData: UpsertQueryDataThunk<
218+
Definitions,
219+
RootState<Definitions, string, ReducerPath>
220+
>
213221
/**
214222
* A Redux thunk that applies a JSON diff/patch array to the cached data for a given query result. This immediately updates the Redux state with those changes.
215223
*
@@ -416,6 +424,7 @@ export const coreModule = (): Module<CoreModule> => ({
416424
mutationThunk,
417425
patchQueryData,
418426
updateQueryData,
427+
upsertQueryData,
419428
prefetch,
420429
buildMatchThunkActions,
421430
} = buildThunks({
@@ -444,6 +453,7 @@ export const coreModule = (): Module<CoreModule> => ({
444453
safeAssign(api.util, {
445454
patchQueryData,
446455
updateQueryData,
456+
upsertQueryData,
447457
prefetch,
448458
resetApiState: sliceActions.resetApiState,
449459
})

0 commit comments

Comments
 (0)