Skip to content

Commit 2d000a5

Browse files
authored
Merge pull request #2663 from schadenn/feature/add-side-effect-forced
2 parents 7c4348c + 9723738 commit 2d000a5

File tree

4 files changed

+112
-11
lines changed

4 files changed

+112
-11
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@ import type {
55
QueryArgFrom,
66
ResultTypeFrom,
77
} from '../endpointDefinitions'
8-
import { DefinitionType } from '../endpointDefinitions'
8+
import { DefinitionType, isQueryDefinition } from '../endpointDefinitions'
99
import type { QueryThunk, MutationThunk, QueryThunkArg } from './buildThunks'
1010
import type { AnyAction, ThunkAction, SerializedError } from '@reduxjs/toolkit'
1111
import type { SubscriptionOptions, RootState } from './apiState'
12-
import { QueryStatus } from './apiState'
1312
import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs'
1413
import type { Api, ApiContext } from '../apiTypes'
1514
import type { ApiEndpointQuery } from './module'
@@ -279,10 +278,11 @@ Features like automatic cache collection, automatic refetching etc. will not be
279278
endpointDefinition,
280279
endpointName,
281280
})
281+
282282
const thunk = queryThunk({
283283
type: 'query',
284284
subscribe,
285-
forceRefetch,
285+
forceRefetch: forceRefetch,
286286
subscriptionOptions,
287287
endpointName,
288288
originalArgs: arg,

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

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ import type {
1212
QueryActionCreatorResult,
1313
} from './buildInitiate'
1414
import { forceQueryFnSymbol, isUpsertQuery } from './buildInitiate'
15-
import type {
15+
import {
1616
AssertTagTypes,
1717
EndpointDefinition,
1818
EndpointDefinitions,
19+
isQueryDefinition,
1920
MutationDefinition,
2021
QueryArgFrom,
2122
QueryDefinition,
@@ -474,26 +475,51 @@ In the case of an unhandled error, no tags will be "provided" or "invalidated".`
474475
getPendingMeta() {
475476
return { startedTimeStamp: Date.now() }
476477
},
477-
condition(arg, { getState }) {
478+
condition(queryThunkArgs, { getState }) {
478479
const state = getState()
479-
const requestState = state[reducerPath]?.queries?.[arg.queryCacheKey]
480+
481+
const requestState =
482+
state[reducerPath]?.queries?.[queryThunkArgs.queryCacheKey]
480483
const fulfilledVal = requestState?.fulfilledTimeStamp
484+
const currentArg = queryThunkArgs.originalArgs
485+
const previousArg = requestState?.originalArgs
486+
const endpointDefinition =
487+
endpointDefinitions[queryThunkArgs.endpointName]
481488

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

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

490501
// if this is forced, continue
491-
if (isForcedQuery(arg, state)) return true
502+
if (isForcedQuery(queryThunkArgs, state)) {
503+
return true
504+
}
505+
506+
if (
507+
isQueryDefinition(endpointDefinition) &&
508+
endpointDefinition?.forceRefetch?.({
509+
currentArg,
510+
previousArg,
511+
endpointState: requestState,
512+
state,
513+
})
514+
) {
515+
return true
516+
}
492517

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

498524
return true
499525
},

packages/toolkit/src/query/endpointDefinitions.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { AnyAction, ThunkDispatch } from '@reduxjs/toolkit'
22
import { SerializeQueryArgs } from './defaultSerializeQueryArgs'
3-
import type { RootState } from './core/apiState'
3+
import type { QuerySubState, RootState } from './core/apiState'
44
import type {
55
BaseQueryExtraOptions,
66
BaseQueryFn,
@@ -339,6 +339,36 @@ 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+
*/
365+
forceRefetch?(params: {
366+
currentArg: QueryArg | undefined
367+
previousArg: QueryArg | undefined
368+
state: RootState<any, any, string>
369+
endpointState?: QuerySubState<any>
370+
}): boolean
371+
342372
/**
343373
* All of these are `undefined` at runtime, purely to be used in TypeScript declarations!
344374
*/

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

Lines changed: 45 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,37 @@ 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+
const pageNum = parseInt(pageString || '0')
947+
948+
const results = paginate(allItems, PAGE_SIZE, pageNum)
949+
return res(ctx.json(results))
950+
})
951+
)
952+
953+
// Page number shouldn't matter here, because the cache key ignores that.
954+
// We just need to select the only cache entry.
955+
const selectListItems = api.endpoints.listItems.select(0)
956+
957+
await storeRef.store.dispatch(api.endpoints.listItems.initiate(1))
958+
959+
const initialEntry = selectListItems(storeRef.store.getState())
960+
expect(initialEntry.data).toEqual(['a', 'b', 'c'])
961+
962+
await storeRef.store.dispatch(api.endpoints.listItems.initiate(2))
963+
const updatedEntry = selectListItems(storeRef.store.getState())
964+
expect(updatedEntry.data).toEqual(['a', 'b', 'c', 'd', 'e', 'f'])
965+
})
921966
})

0 commit comments

Comments
 (0)