Skip to content

Commit 4f3bc9f

Browse files
authored
Ensure upserted cache entries always get written (#4768)
1 parent 6590cec commit 4f3bc9f

File tree

2 files changed

+100
-8
lines changed

2 files changed

+100
-8
lines changed

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

+13-6
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export type ProcessedQueryUpsertEntry = {
8484
/**
8585
* A typesafe representation of a util action creator that accepts cache entry descriptions to upsert
8686
*/
87-
export type UpsertEntries<Definitions extends EndpointDefinitions> = <
87+
export type UpsertEntries<Definitions extends EndpointDefinitions> = (<
8888
EndpointNames extends Array<QueryKeys<Definitions>>,
8989
>(
9090
entries: [
@@ -95,7 +95,11 @@ export type UpsertEntries<Definitions extends EndpointDefinitions> = <
9595
>
9696
},
9797
],
98-
) => PayloadAction<NormalizedQueryUpsertEntryPayload[]>
98+
) => PayloadAction<NormalizedQueryUpsertEntryPayload[]>) & {
99+
match: (
100+
action: unknown,
101+
) => action is PayloadAction<NormalizedQueryUpsertEntryPayload[]>
102+
}
99103

100104
function updateQuerySubstateIfExists(
101105
state: QueryState<any>,
@@ -212,10 +216,10 @@ export function buildSlice({
212216
// RTK_autoBatch: true
213217
},
214218
payload: unknown,
219+
upserting: boolean,
215220
) {
216221
updateQuerySubstateIfExists(draft, meta.arg.queryCacheKey, (substate) => {
217-
if (substate.requestId !== meta.requestId && !isUpsertQuery(meta.arg))
218-
return
222+
if (substate.requestId !== meta.requestId && !upserting) return
219223
const { merge } = definitions[meta.arg.endpointName] as QueryDefinition<
220224
any,
221225
any,
@@ -248,7 +252,7 @@ export function buildSlice({
248252
} else {
249253
// Assign or safely update the cache data.
250254
substate.data =
251-
definitions[meta.arg.endpointName].structuralSharing ?? true
255+
(definitions[meta.arg.endpointName].structuralSharing ?? true)
252256
? copyWithStructuralSharing(
253257
isDraft(substate.data)
254258
? original(substate.data)
@@ -307,6 +311,8 @@ export function buildSlice({
307311
baseQueryMeta: {},
308312
},
309313
value,
314+
// We know we're upserting here
315+
true,
310316
)
311317
}
312318
},
@@ -365,7 +371,8 @@ export function buildSlice({
365371
writePendingCacheEntry(draft, arg, upserting, meta)
366372
})
367373
.addCase(queryThunk.fulfilled, (draft, { meta, payload }) => {
368-
writeFulfilledCacheEntry(draft, meta, payload)
374+
const upserting = isUpsertQuery(meta.arg)
375+
writeFulfilledCacheEntry(draft, meta, payload, upserting)
369376
})
370377
.addCase(
371378
queryThunk.rejected,

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

+87-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ import {
55
hookWaitFor,
66
setupApiStore,
77
} from '../../tests/utils/helpers'
8-
import { renderHook, act, waitFor } from '@testing-library/react'
8+
import {
9+
render,
10+
renderHook,
11+
act,
12+
waitFor,
13+
screen,
14+
} from '@testing-library/react'
915
import { delay } from 'msw'
1016

1117
interface Post {
@@ -14,6 +20,11 @@ interface Post {
1420
contents: string
1521
}
1622

23+
interface FolderT {
24+
id: number
25+
children: FolderT[]
26+
}
27+
1728
const baseQuery = vi.fn()
1829
beforeEach(() => baseQuery.mockReset())
1930

@@ -28,7 +39,7 @@ const api = createApi({
2839
.catch((e: any) => ({ error: e }))
2940
return { data: result, meta: 'meta' }
3041
},
31-
tagTypes: ['Post'],
42+
tagTypes: ['Post', 'Folder'],
3243
endpoints: (build) => ({
3344
getPosts: build.query<Post[], void>({
3445
query: () => '/posts',
@@ -80,6 +91,30 @@ const api = createApi({
8091
},
8192
keepUnusedDataFor: 0.01,
8293
}),
94+
getFolder: build.query<FolderT, number>({
95+
queryFn: async (args) => {
96+
return {
97+
data: {
98+
id: args,
99+
// Folder contains children that are as well folders
100+
children: [{ id: 2, children: [] }],
101+
},
102+
}
103+
},
104+
providesTags: (result, err, args) => [{ type: 'Folder', id: args }],
105+
onQueryStarted: async (args, queryApi) => {
106+
const { data } = await queryApi.queryFulfilled
107+
108+
// Upsert getFolder endpoint with children from response data
109+
const upsertData = data.children.map((child) => ({
110+
arg: child.id,
111+
endpointName: 'getFolder' as const,
112+
value: child,
113+
}))
114+
115+
queryApi.dispatch(api.util.upsertQueryEntries(upsertData))
116+
},
117+
}),
83118
}),
84119
})
85120

@@ -434,6 +469,56 @@ describe('upsertQueryEntries', () => {
434469
undefined,
435470
)
436471
})
472+
473+
test('Handles repeated upserts and async lifecycles', async () => {
474+
const StateForUpsertFolder = ({ folderId }: { folderId: number }) => {
475+
const { status } = api.useGetFolderQuery(folderId)
476+
477+
return (
478+
<>
479+
<div>
480+
Status getFolder with ID (
481+
{folderId === 1 ? 'original request' : 'upserted'}) {folderId}:{' '}
482+
<span data-testid={`status-${folderId}`}>{status}</span>
483+
</div>
484+
</>
485+
)
486+
}
487+
488+
const Folder = () => {
489+
const { data, isLoading, isError } = api.useGetFolderQuery(1)
490+
491+
return (
492+
<div>
493+
<h1>Folders</h1>
494+
495+
{isLoading && <div>Loading...</div>}
496+
497+
{isError && <div>Error...</div>}
498+
499+
<StateForUpsertFolder key={`state-${1}`} folderId={1} />
500+
<StateForUpsertFolder key={`state-${2}`} folderId={2} />
501+
</div>
502+
)
503+
}
504+
505+
render(<Folder />, {
506+
wrapper: storeRef.wrapper,
507+
})
508+
509+
await waitFor(() => {
510+
const { actions } = storeRef.store.getState()
511+
// Inspection:
512+
// - 2 inits
513+
// - 2 pendings, 2 fulfilleds for the hook queries
514+
// - 2 upserts
515+
expect(actions.length).toBe(8)
516+
expect(
517+
actions.filter((a) => api.util.upsertQueryEntries.match(a)).length,
518+
).toBe(2)
519+
})
520+
expect(screen.getByTestId('status-2').textContent).toBe('fulfilled')
521+
})
437522
})
438523

439524
describe('full integration', () => {

0 commit comments

Comments
 (0)