Skip to content

Commit

Permalink
Merge pull request #2663 from schadenn/feature/add-side-effect-forced
Browse files Browse the repository at this point in the history
  • Loading branch information
markerikson committed Oct 8, 2022
2 parents 7c4348c + 9723738 commit 2d000a5
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 11 deletions.
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'])
})
})

0 comments on commit 2d000a5

Please sign in to comment.