diff --git a/packages/toolkit/src/query/core/buildThunks.ts b/packages/toolkit/src/query/core/buildThunks.ts index 01d76716d7..67291aad07 100644 --- a/packages/toolkit/src/query/core/buildThunks.ts +++ b/packages/toolkit/src/query/core/buildThunks.ts @@ -475,36 +475,51 @@ In the case of an unhandled error, no tags will be "provided" or "invalidated".` getPendingMeta() { return { startedTimeStamp: Date.now() } }, - condition(arg, { getState }) { + condition(queryThunkArgs, { getState }) { const state = getState() - const requestState = state[reducerPath]?.queries?.[arg.queryCacheKey] + + const requestState = + state[reducerPath]?.queries?.[queryThunkArgs.queryCacheKey] const fulfilledVal = requestState?.fulfilledTimeStamp - const endpointDefinition = endpointDefinitions[arg.endpointName] + const currentArg = queryThunkArgs.originalArgs + const previousArg = requestState?.originalArgs + const endpointDefinition = + endpointDefinitions[queryThunkArgs.endpointName] // Order of these checks matters. // In order for `upsertQueryData` to successfully run while an existing request is in flight, /// we have to check for that first, otherwise `queryThunk` will bail out and not run at all. - if (isUpsertQuery(arg)) return true + if (isUpsertQuery(queryThunkArgs)) { + return true + } // Don't retry a request that's currently in-flight - if (requestState?.status === 'pending') return false + if (requestState?.status === 'pending') { + return false + } // if this is forced, continue - if (isForcedQuery(arg, state)) return true + if (isForcedQuery(queryThunkArgs, state)) { + return true + } if ( isQueryDefinition(endpointDefinition) && endpointDefinition?.forceRefetch?.({ + currentArg, + previousArg, endpointState: requestState, state, }) - ) + ) { return true + } // Pull from the cache unless we explicitly force refetch or qualify based on time - if (fulfilledVal) + if (fulfilledVal) { // Value is cached and we didn't specify to refresh, skip it. return false + } return true }, diff --git a/packages/toolkit/src/query/endpointDefinitions.ts b/packages/toolkit/src/query/endpointDefinitions.ts index 59eda3c7fa..f0bc6c7534 100644 --- a/packages/toolkit/src/query/endpointDefinitions.ts +++ b/packages/toolkit/src/query/endpointDefinitions.ts @@ -339,7 +339,32 @@ export interface QueryExtraOptions< responseData: ResultType ): ResultType | void + /** + * Check to see if the endpoint should force a refetch in cases where it normally wouldn't. + * This is primarily useful for "infinite scroll" / pagination use cases where + * RTKQ is keeping a single cache entry that is added to over time, in combination + * with `serializeQueryArgs` returning a fixed cache key and a `merge` callback + * set to add incoming data to the cache entry each time. + * + * Example: + * + * ```ts + * forceRefetch({currentArg, previousArg}) { + * // Assume these are page numbers + * return currentArg !== previousArg + * }, + * serializeQueryArgs({endpointName}) { + * return endpointName + * }, + * merge(currentCacheData, responseData) { + * currentCacheData.push(...responseData) + * } + * + * ``` + */ forceRefetch?(params: { + currentArg: QueryArg | undefined + previousArg: QueryArg | undefined state: RootState endpointState?: QuerySubState }): boolean diff --git a/packages/toolkit/src/query/tests/createApi.test.ts b/packages/toolkit/src/query/tests/createApi.test.ts index ed0c0e9cb7..ba7671dead 100644 --- a/packages/toolkit/src/query/tests/createApi.test.ts +++ b/packages/toolkit/src/query/tests/createApi.test.ts @@ -866,6 +866,18 @@ describe('custom serializeQueryArgs per endpoint', () => { query: (arg) => `${arg}`, serializeQueryArgs: serializer1, }), + listItems: build.query({ + query: (pageNumber) => `/listItems?page=${pageNumber}`, + serializeQueryArgs: ({ endpointName }) => { + return endpointName + }, + merge: (currentCache, newItems) => { + currentCache.push(...newItems) + }, + forceRefetch({ currentArg, previousArg }) { + return currentArg !== previousArg + }, + }), }), }) @@ -918,4 +930,39 @@ describe('custom serializeQueryArgs per endpoint', () => { ] ).toBeTruthy() }) + + test('serializeQueryArgs + merge allows refetching as args change with same cache key', async () => { + const allItems = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'i'] + const PAGE_SIZE = 3 + + function paginate(array: T[], page_size: number, page_number: number) { + // human-readable page numbers usually start with 1, so we reduce 1 in the first argument + return array.slice((page_number - 1) * page_size, page_number * page_size) + } + + server.use( + rest.get('https://example.com/listItems', (req, res, ctx) => { + const pageString = req.url.searchParams.get('page') + console.log('Page string: ', pageString) + const pageNum = parseInt(pageString || '0') + + const results = paginate(allItems, PAGE_SIZE, pageNum) + console.log('Page num: ', pageNum, 'Results: ', results) + return res(ctx.json(results)) + }) + ) + + // Page number shouldn't matter here, because the cache key ignores that. + // We just need to select the only cache entry. + const selectListItems = api.endpoints.listItems.select(0) + + await storeRef.store.dispatch(api.endpoints.listItems.initiate(1)) + + const initialEntry = selectListItems(storeRef.store.getState()) + expect(initialEntry.data).toEqual(['a', 'b', 'c']) + + await storeRef.store.dispatch(api.endpoints.listItems.initiate(2)) + const updatedEntry = selectListItems(storeRef.store.getState()) + expect(updatedEntry.data).toEqual(['a', 'b', 'c', 'd', 'e', 'f']) + }) })