Skip to content

Commit 9f12efd

Browse files
committed
Enable upsertQueryData calls while a request is in flight
1 parent 936493e commit 9f12efd

File tree

3 files changed

+81
-8
lines changed

3 files changed

+81
-8
lines changed

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: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { QueryStatus } from './apiState'
1010
import {
1111
forceQueryFnSymbol,
1212
StartQueryActionCreatorOptions,
13+
QueryActionCreatorResult,
1314
} from './buildInitiate'
1415
import type {
1516
AssertTagTypes,
@@ -176,7 +177,12 @@ export type UpsertQueryDataThunk<
176177
endpointName: EndpointName,
177178
args: QueryArgFrom<Definitions[EndpointName]>,
178179
value: ResultTypeFrom<Definitions[EndpointName]>
179-
) => ThunkAction<void, PartialState, any, AnyAction>
180+
) => ThunkAction<
181+
QueryActionCreatorResult<Definitions[EndpointName]>,
182+
PartialState,
183+
any,
184+
AnyAction
185+
>
180186

181187
/**
182188
* An object returned from dispatching a `api.util.updateQueryData` call.
@@ -272,7 +278,7 @@ export function buildThunks<
272278

273279
const upsertQueryData: UpsertQueryDataThunk<EndpointDefinitions, State> =
274280
(endpointName, args, value) => (dispatch) => {
275-
dispatch(
281+
return dispatch(
276282
(
277283
api.endpoints[endpointName] as ApiEndpointQuery<
278284
QueryDefinition<any, any, any, any, any>,
@@ -469,12 +475,17 @@ In the case of an unhandled error, no tags will be "provided" or "invalidated".`
469475
const requestState = state[reducerPath]?.queries?.[arg.queryCacheKey]
470476
const fulfilledVal = requestState?.fulfilledTimeStamp
471477

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

475483
// if this is forced, continue
476484
if (isForcedQuery(arg, state)) return true
477485

486+
// Don't retry a request that's currently in-flight
487+
if (requestState?.status === 'pending') return false
488+
478489
// Pull from the cache unless we explicitly force refetch or qualify based on time
479490
if (fulfilledVal)
480491
// Value is cached and we didn't specify to refresh, skip it.

packages/toolkit/src/query/tests/optimisticUpserts.test.tsx

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createApi } from '@reduxjs/toolkit/query/react'
22
import { actionsReducer, hookWaitFor, setupApiStore, waitMs } from './helpers'
33
import { skipToken } from '../core/buildSelectors'
4-
import { renderHook, act } from '@testing-library/react'
4+
import { renderHook, act, waitFor } from '@testing-library/react'
55

66
interface Post {
77
id: string
@@ -12,6 +12,10 @@ interface Post {
1212
const baseQuery = jest.fn()
1313
beforeEach(() => baseQuery.mockReset())
1414

15+
function delay(ms: number) {
16+
return new Promise((resolve) => setTimeout(resolve, ms))
17+
}
18+
1519
const api = createApi({
1620
baseQuery: (...args: any[]) => {
1721
const result = baseQuery(...args)
@@ -46,6 +50,18 @@ const api = createApi({
4650
},
4751
invalidatesTags: (result) => (result ? ['Post'] : []),
4852
}),
53+
post2: build.query<Post, string>({
54+
queryFn: async (id) => {
55+
await delay(20)
56+
return {
57+
data: {
58+
id,
59+
title: 'All about cheese.',
60+
contents: 'TODO',
61+
},
62+
}
63+
},
64+
}),
4965
}),
5066
})
5167

@@ -195,10 +211,9 @@ describe('upsertQueryData', () => {
195211
)
196212
await hookWaitFor(() => expect(result.current.isError).toBeTruthy())
197213

198-
let returnValue!: ReturnType<ReturnType<typeof api.util.upsertQueryData>>
199214
// upsert the data
200215
act(() => {
201-
returnValue = storeRef.store.dispatch(
216+
storeRef.store.dispatch(
202217
api.util.upsertQueryData('post', '4', {
203218
id: '4',
204219
title: 'All about cheese',
@@ -351,4 +366,44 @@ describe('full integration', () => {
351366
50
352367
)
353368
})
369+
370+
test.only('Interop with in-flight requests', async () => {
371+
await act(async () => {
372+
const fetchRes = storeRef.store.dispatch(
373+
api.endpoints.post2.initiate('3')
374+
)
375+
376+
const upsertRes = storeRef.store.dispatch(
377+
api.util.upsertQueryData('post2', '3', {
378+
id: '3',
379+
title: 'Upserted title',
380+
contents: 'Upserted contents',
381+
})
382+
)
383+
384+
const selectEntry = api.endpoints.post2.select('3')
385+
await waitFor(
386+
() => {
387+
const entry1 = selectEntry(storeRef.store.getState())
388+
expect(entry1.data).toEqual({
389+
id: '3',
390+
title: 'Upserted title',
391+
contents: 'Upserted contents',
392+
})
393+
},
394+
{ interval: 1, timeout: 15 }
395+
)
396+
await waitFor(
397+
() => {
398+
const entry2 = selectEntry(storeRef.store.getState())
399+
expect(entry2.data).toEqual({
400+
id: '3',
401+
title: 'All about cheese.',
402+
contents: 'TODO',
403+
})
404+
},
405+
{ interval: 1 }
406+
)
407+
})
408+
})
354409
})

0 commit comments

Comments
 (0)