Skip to content

Commit d0118bb

Browse files
committed
Add args to forceRefetch, add doc block, and test for merging
1 parent 0dedba0 commit d0118bb

File tree

3 files changed

+95
-8
lines changed

3 files changed

+95
-8
lines changed

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

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -475,36 +475,51 @@ In the case of an unhandled error, no tags will be "provided" or "invalidated".`
475475
getPendingMeta() {
476476
return { startedTimeStamp: Date.now() }
477477
},
478-
condition(arg, { getState }) {
478+
condition(queryThunkArgs, { getState }) {
479479
const state = getState()
480-
const requestState = state[reducerPath]?.queries?.[arg.queryCacheKey]
480+
481+
const requestState =
482+
state[reducerPath]?.queries?.[queryThunkArgs.queryCacheKey]
481483
const fulfilledVal = requestState?.fulfilledTimeStamp
482-
const endpointDefinition = endpointDefinitions[arg.endpointName]
484+
const currentArg = queryThunkArgs.originalArgs
485+
const previousArg = requestState?.originalArgs
486+
const endpointDefinition =
487+
endpointDefinitions[queryThunkArgs.endpointName]
483488

484489
// Order of these checks matters.
485490
// In order for `upsertQueryData` to successfully run while an existing request is in flight,
486491
/// we have to check for that first, otherwise `queryThunk` will bail out and not run at all.
487-
if (isUpsertQuery(arg)) return true
492+
if (isUpsertQuery(queryThunkArgs)) {
493+
return true
494+
}
488495

489496
// Don't retry a request that's currently in-flight
490-
if (requestState?.status === 'pending') return false
497+
if (requestState?.status === 'pending') {
498+
return false
499+
}
491500

492501
// if this is forced, continue
493-
if (isForcedQuery(arg, state)) return true
502+
if (isForcedQuery(queryThunkArgs, state)) {
503+
return true
504+
}
494505

495506
if (
496507
isQueryDefinition(endpointDefinition) &&
497508
endpointDefinition?.forceRefetch?.({
509+
currentArg,
510+
previousArg,
498511
endpointState: requestState,
499512
state,
500513
})
501-
)
514+
) {
502515
return true
516+
}
503517

504518
// Pull from the cache unless we explicitly force refetch or qualify based on time
505-
if (fulfilledVal)
519+
if (fulfilledVal) {
506520
// Value is cached and we didn't specify to refresh, skip it.
507521
return false
522+
}
508523

509524
return true
510525
},

packages/toolkit/src/query/endpointDefinitions.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,32 @@ export interface QueryExtraOptions<
339339
responseData: ResultType
340340
): ResultType | void
341341

342+
/**
343+
* Check to see if the endpoint should force a refetch in cases where it normally wouldn't.
344+
* This is primarily useful for "infinite scroll" / pagination use cases where
345+
* RTKQ is keeping a single cache entry that is added to over time, in combination
346+
* with `serializeQueryArgs` returning a fixed cache key and a `merge` callback
347+
* set to add incoming data to the cache entry each time.
348+
*
349+
* Example:
350+
*
351+
* ```ts
352+
* forceRefetch({currentArg, previousArg}) {
353+
* // Assume these are page numbers
354+
* return currentArg !== previousArg
355+
* },
356+
* serializeQueryArgs({endpointName}) {
357+
* return endpointName
358+
* },
359+
* merge(currentCacheData, responseData) {
360+
* currentCacheData.push(...responseData)
361+
* }
362+
*
363+
* ```
364+
*/
342365
forceRefetch?(params: {
366+
currentArg: QueryArg | undefined
367+
previousArg: QueryArg | undefined
343368
state: RootState<any, any, string>
344369
endpointState?: QuerySubState<any>
345370
}): boolean

packages/toolkit/src/query/tests/createApi.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,18 @@ describe('custom serializeQueryArgs per endpoint', () => {
866866
query: (arg) => `${arg}`,
867867
serializeQueryArgs: serializer1,
868868
}),
869+
listItems: build.query<string[], number>({
870+
query: (pageNumber) => `/listItems?page=${pageNumber}`,
871+
serializeQueryArgs: ({ endpointName }) => {
872+
return endpointName
873+
},
874+
merge: (currentCache, newItems) => {
875+
currentCache.push(...newItems)
876+
},
877+
forceRefetch({ currentArg, previousArg }) {
878+
return currentArg !== previousArg
879+
},
880+
}),
869881
}),
870882
})
871883

@@ -918,4 +930,39 @@ describe('custom serializeQueryArgs per endpoint', () => {
918930
]
919931
).toBeTruthy()
920932
})
933+
934+
test('serializeQueryArgs + merge allows refetching as args change with same cache key', async () => {
935+
const allItems = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'i']
936+
const PAGE_SIZE = 3
937+
938+
function paginate<T>(array: T[], page_size: number, page_number: number) {
939+
// human-readable page numbers usually start with 1, so we reduce 1 in the first argument
940+
return array.slice((page_number - 1) * page_size, page_number * page_size)
941+
}
942+
943+
server.use(
944+
rest.get('https://example.com/listItems', (req, res, ctx) => {
945+
const pageString = req.url.searchParams.get('page')
946+
console.log('Page string: ', pageString)
947+
const pageNum = parseInt(pageString || '0')
948+
949+
const results = paginate(allItems, PAGE_SIZE, pageNum)
950+
console.log('Page num: ', pageNum, 'Results: ', results)
951+
return res(ctx.json(results))
952+
})
953+
)
954+
955+
// Page number shouldn't matter here, because the cache key ignores that.
956+
// We just need to select the only cache entry.
957+
const selectListItems = api.endpoints.listItems.select(0)
958+
959+
await storeRef.store.dispatch(api.endpoints.listItems.initiate(1))
960+
961+
const initialEntry = selectListItems(storeRef.store.getState())
962+
expect(initialEntry.data).toEqual(['a', 'b', 'c'])
963+
964+
await storeRef.store.dispatch(api.endpoints.listItems.initiate(2))
965+
const updatedEntry = selectListItems(storeRef.store.getState())
966+
expect(updatedEntry.data).toEqual(['a', 'b', 'c', 'd', 'e', 'f'])
967+
})
921968
})

0 commit comments

Comments
 (0)