Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add forceRefetch to QueryExtraOptions #2663

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/toolkit/src/query/core/buildInitiate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ import type {
QueryArgFrom,
ResultTypeFrom,
} from '../endpointDefinitions'
import { DefinitionType } from '../endpointDefinitions'
import { DefinitionType, isQueryDefinition } from '../endpointDefinitions'
import type { QueryThunk, MutationThunk, QueryThunkArg } from './buildThunks'
import type { AnyAction, ThunkAction, SerializedError } from '@reduxjs/toolkit'
import type { SubscriptionOptions, RootState } from './apiState'
import { QueryStatus } from './apiState'
import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs'
import type { Api, ApiContext } from '../apiTypes'
import type { ApiEndpointQuery } from './module'
Expand Down Expand Up @@ -279,10 +278,11 @@ Features like automatic cache collection, automatic refetching etc. will not be
endpointDefinition,
endpointName,
})

const thunk = queryThunk({
type: 'query',
subscribe,
forceRefetch,
forceRefetch: forceRefetch,
subscriptionOptions,
endpointName,
originalArgs: arg,
Expand Down
40 changes: 33 additions & 7 deletions packages/toolkit/src/query/core/buildThunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import type {
QueryActionCreatorResult,
} from './buildInitiate'
import { forceQueryFnSymbol, isUpsertQuery } from './buildInitiate'
import type {
import {
AssertTagTypes,
EndpointDefinition,
EndpointDefinitions,
isQueryDefinition,
MutationDefinition,
QueryArgFrom,
QueryDefinition,
Expand Down Expand Up @@ -474,26 +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 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
},
Expand Down
32 changes: 31 additions & 1 deletion packages/toolkit/src/query/endpointDefinitions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { AnyAction, ThunkDispatch } from '@reduxjs/toolkit'
import { SerializeQueryArgs } from './defaultSerializeQueryArgs'
import type { RootState } from './core/apiState'
import type { QuerySubState, RootState } from './core/apiState'
import type {
BaseQueryExtraOptions,
BaseQueryFn,
Expand Down Expand Up @@ -339,6 +339,36 @@ 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<any, any, string>
endpointState?: QuerySubState<any>
}): boolean

/**
* All of these are `undefined` at runtime, purely to be used in TypeScript declarations!
*/
Expand Down
45 changes: 45 additions & 0 deletions packages/toolkit/src/query/tests/createApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,18 @@ describe('custom serializeQueryArgs per endpoint', () => {
query: (arg) => `${arg}`,
serializeQueryArgs: serializer1,
}),
listItems: build.query<string[], number>({
query: (pageNumber) => `/listItems?page=${pageNumber}`,
serializeQueryArgs: ({ endpointName }) => {
return endpointName
},
merge: (currentCache, newItems) => {
currentCache.push(...newItems)
},
forceRefetch({ currentArg, previousArg }) {
return currentArg !== previousArg
},
}),
}),
})

Expand Down Expand Up @@ -918,4 +930,37 @@ 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<T>(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')
const pageNum = parseInt(pageString || '0')

const results = paginate(allItems, PAGE_SIZE, pageNum)
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'])
})
})