Skip to content

Commit f9b23fc

Browse files
authored
feat(infiniteQuery): add possibility to decide which pages should be refetched (TanStack#2557)
1 parent 3dac094 commit f9b23fc

File tree

8 files changed

+297
-29
lines changed

8 files changed

+297
-29
lines changed

docs/src/pages/guides/infinite-queries.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,27 @@ function Projects() {
9595

9696
When an infinite query becomes `stale` and needs to be refetched, each group is fetched `sequentially`, starting from the first one. This ensures that even if the underlying data is mutated, we're not using stale cursors and potentially getting duplicates or skipping records. If an infinite query's results are ever removed from the queryCache, the pagination restarts at the initial state with only the initial group being requested.
9797

98+
### refetchPage
99+
100+
If you only want to actively refetch a subset of all pages, you can pass the `refetchPage` function to `refetch` returned from `useInfiniteQuery`.
101+
102+
```js
103+
const { refetch } = useInfiniteQuery('projects', fetchProjects, {
104+
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
105+
})
106+
107+
// only refetch the first page
108+
refetch({ refetchPage: (page, index) => index === 0 })
109+
```
110+
111+
You can also pass this function as part of the 2nd argument (`queryFilters`) to [queryClient.refetchQueries](/reference/QueryClient#queryclientrefetchqueries), [queryClient.invalidateQueries](/reference/QueryClient#queryclientinvalidatequeries) or [queryClient.resetQueries](/reference/QueryClient#queryclientresetqueries).
112+
113+
**Signature**
114+
115+
- `refetchPage: (page: TData, index: number, allPages: TData[]) => boolean`
116+
117+
The function is executed for each page, and only pages where this function returns `true` will be refetched.
118+
98119
## What if I need to pass custom information to my query function?
99120

100121
By default, the variable returned from `getNextPageParam` will be supplied to the query function, but in some cases, you may want to override this. You can pass custom variables to the `fetchNextPage` function which will override the default variable like so:

docs/src/pages/reference/QueryClient.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ const data = queryClient.getQueriesData(queryKey | filters)
194194

195195
- `[queryKey:QueryKey, data:TData | unknown][]`
196196
- An array of tuples for the matched query keys, or `[]` if there are no matches. The tuples are the query key and its associated data.
197-
197+
198198
**Caveats**
199199

200200
Because the returned data in each tuple can be of varying structures (i.e. using a filter to return "active" queries can return different data types), the `TData` generic defaults to `unknown`. If you provide a more specific type to `TData` it is assumed that you are certain each tuple's data entry is all the same type.
@@ -287,6 +287,9 @@ await queryClient.invalidateQueries('posts', {
287287
- `refetchInactive: Boolean`
288288
- Defaults to `false`
289289
- When set to `true`, queries that match the refetch predicate and are not being rendered via `useQuery` and friends will be both marked as invalid and also refetched in the background
290+
- `refetchPage: (page: TData, index: number, allPages: TData[]) => boolean`
291+
- Only for [Infinite Queries](../guides/infinite-queries#refetchpage)
292+
- Use this function to specify which pages should be refetched
290293
- `refetchOptions?: RefetchOptions`:
291294
- `throwOnError?: boolean`
292295
- When set to `true`, this method will throw if any of the query refetch tasks fail.
@@ -315,6 +318,9 @@ await queryClient.refetchQueries(['posts', 1], { active: true, exact: true })
315318

316319
- `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
317320
- `filters?: QueryFilters`: [Query Filters](../guides/filters#query-filters)
321+
- `refetchPage: (page: TData, index: number, allPages: TData[]) => boolean`
322+
- Only for [Infinite Queries](../guides/infinite-queries#refetchpage)
323+
- Use this function to specify which pages should be refetched
318324
- `refetchOptions?: RefetchOptions`:
319325
- `throwOnError?: boolean`
320326
- When set to `true`, this method will throw if any of the query refetch tasks fail.
@@ -378,6 +384,9 @@ queryClient.resetQueries(queryKey, { exact: true })
378384

379385
- `queryKey?: QueryKey`: [Query Keys](../guides/query-keys)
380386
- `filters?: QueryFilters`: [Query Filters](../guides/filters#query-filters)
387+
- `refetchPage: (page: TData, index: number, allPages: TData[]) => boolean`
388+
- Only for [Infinite Queries](../guides/infinite-queries#refetchpage)
389+
- Use this function to specify which pages should be refetched
381390
- `resetOptions?: ResetOptions`:
382391
- `throwOnError?: boolean`
383392
- When set to `true`, this method will throw if any of the query refetch tasks fail.

src/core/infiniteQueryBehavior.ts

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import type { QueryBehavior } from './query'
22
import { isCancelable } from './retryer'
3-
import type { InfiniteData, QueryFunctionContext, QueryOptions } from './types'
3+
import type {
4+
InfiniteData,
5+
QueryFunctionContext,
6+
QueryOptions,
7+
RefetchQueryFilters,
8+
} from './types'
49

510
export function infiniteQueryBehavior<
611
TQueryFnData,
@@ -10,6 +15,8 @@ export function infiniteQueryBehavior<
1015
return {
1116
onFetch: context => {
1217
context.fetchFn = () => {
18+
const refetchPage: RefetchQueryFilters['refetchPage'] | undefined =
19+
context.fetchOptions?.meta?.refetchPage
1320
const fetchMore = context.fetchOptions?.meta?.fetchMore
1421
const pageParam = fetchMore?.pageParam
1522
const isFetchingNextPage = fetchMore?.direction === 'forward'
@@ -23,6 +30,18 @@ export function infiniteQueryBehavior<
2330
const queryFn =
2431
context.options.queryFn || (() => Promise.reject('Missing queryFn'))
2532

33+
const buildNewPages = (
34+
pages: unknown[],
35+
param: unknown,
36+
page: unknown,
37+
previous?: boolean
38+
) => {
39+
newPageParams = previous
40+
? [param, ...newPageParams]
41+
: [...newPageParams, param]
42+
return previous ? [page, ...pages] : [...pages, page]
43+
}
44+
2645
// Create function to fetch a page
2746
const fetchPage = (
2847
pages: unknown[],
@@ -45,12 +64,9 @@ export function infiniteQueryBehavior<
4564

4665
const queryFnResult = queryFn(queryFnContext)
4766

48-
const promise = Promise.resolve(queryFnResult).then(page => {
49-
newPageParams = previous
50-
? [param, ...newPageParams]
51-
: [...newPageParams, param]
52-
return previous ? [page, ...pages] : [...pages, page]
53-
})
67+
const promise = Promise.resolve(queryFnResult).then(page =>
68+
buildNewPages(pages, param, page, previous)
69+
)
5470

5571
if (isCancelable(queryFnResult)) {
5672
const promiseAsAny = promise as any
@@ -91,16 +107,33 @@ export function infiniteQueryBehavior<
91107

92108
const manual = typeof context.options.getNextPageParam === 'undefined'
93109

110+
const shouldFetchFirstPage =
111+
refetchPage && oldPages[0]
112+
? refetchPage(oldPages[0], 0, oldPages)
113+
: true
114+
94115
// Fetch first page
95-
promise = fetchPage([], manual, oldPageParams[0])
116+
promise = shouldFetchFirstPage
117+
? fetchPage([], manual, oldPageParams[0])
118+
: Promise.resolve(buildNewPages([], oldPageParams[0], oldPages[0]))
96119

97120
// Fetch remaining pages
98121
for (let i = 1; i < oldPages.length; i++) {
99122
promise = promise.then(pages => {
100-
const param = manual
101-
? oldPageParams[i]
102-
: getNextPageParam(context.options, pages)
103-
return fetchPage(pages, manual, param)
123+
const shouldFetchNextPage =
124+
refetchPage && oldPages[i]
125+
? refetchPage(oldPages[i], i, oldPages)
126+
: true
127+
128+
if (shouldFetchNextPage) {
129+
const param = manual
130+
? oldPageParams[i]
131+
: getNextPageParam(context.options, pages)
132+
return fetchPage(pages, manual, param)
133+
}
134+
return Promise.resolve(
135+
buildNewPages(pages, oldPageParams[i], oldPages[i])
136+
)
104137
})
105138
}
106139
}

src/core/queryClient.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ import type {
2424
QueryObserverOptions,
2525
QueryOptions,
2626
RefetchOptions,
27+
RefetchQueryFilters,
2728
ResetOptions,
29+
ResetQueryFilters,
2830
} from './types'
2931
import type { QueryState, SetDataOptions } from './query'
3032
import { QueryCache } from './queryCache'
@@ -182,21 +184,24 @@ export class QueryClient {
182184
})
183185
}
184186

185-
resetQueries(filters?: QueryFilters, options?: ResetOptions): Promise<void>
187+
resetQueries(
188+
filters?: ResetQueryFilters,
189+
options?: ResetOptions
190+
): Promise<void>
186191
resetQueries(
187192
queryKey?: QueryKey,
188-
filters?: QueryFilters,
193+
filters?: ResetQueryFilters,
189194
options?: ResetOptions
190195
): Promise<void>
191196
resetQueries(
192-
arg1?: QueryKey | QueryFilters,
193-
arg2?: QueryFilters | ResetOptions,
197+
arg1?: QueryKey | ResetQueryFilters,
198+
arg2?: ResetQueryFilters | ResetOptions,
194199
arg3?: ResetOptions
195200
): Promise<void> {
196201
const [filters, options] = parseFilterArgs(arg1, arg2, arg3)
197202
const queryCache = this.queryCache
198203

199-
const refetchFilters: QueryFilters = {
204+
const refetchFilters: RefetchQueryFilters = {
200205
...filters,
201206
active: true,
202207
}
@@ -249,7 +254,7 @@ export class QueryClient {
249254
): Promise<void> {
250255
const [filters, options] = parseFilterArgs(arg1, arg2, arg3)
251256

252-
const refetchFilters: QueryFilters = {
257+
const refetchFilters: RefetchQueryFilters = {
253258
...filters,
254259
// if filters.refetchActive is not provided and filters.active is explicitly false,
255260
// e.g. invalidateQueries({ active: false }), we don't want to refetch active queries
@@ -266,23 +271,27 @@ export class QueryClient {
266271
}
267272

268273
refetchQueries(
269-
filters?: QueryFilters,
274+
filters?: RefetchQueryFilters,
270275
options?: RefetchOptions
271276
): Promise<void>
272277
refetchQueries(
273278
queryKey?: QueryKey,
274-
filters?: QueryFilters,
279+
filters?: RefetchQueryFilters,
275280
options?: RefetchOptions
276281
): Promise<void>
277282
refetchQueries(
278-
arg1?: QueryKey | QueryFilters,
279-
arg2?: QueryFilters | RefetchOptions,
283+
arg1?: QueryKey | RefetchQueryFilters,
284+
arg2?: RefetchQueryFilters | RefetchOptions,
280285
arg3?: RefetchOptions
281286
): Promise<void> {
282287
const [filters, options] = parseFilterArgs(arg1, arg2, arg3)
283288

284289
const promises = notifyManager.batch(() =>
285-
this.queryCache.findAll(filters).map(query => query.fetch())
290+
this.queryCache.findAll(filters).map(query =>
291+
query.fetch(undefined, {
292+
meta: { refetchPage: filters?.refetchPage },
293+
})
294+
)
286295
)
287296

288297
let promise = Promise.all(promises).then(noop)

src/core/queryObserver.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { RefetchQueryFilters } from './types'
12
import {
23
isServer,
34
isValidTimeout,
@@ -275,9 +276,12 @@ export class QueryObserver<
275276
}
276277

277278
refetch(
278-
options?: RefetchOptions
279+
options?: RefetchOptions & RefetchQueryFilters<TData>
279280
): Promise<QueryObserverResult<TData, TError>> {
280-
return this.fetch(options)
281+
return this.fetch({
282+
...options,
283+
meta: { refetchPage: options?.refetchPage },
284+
})
281285
}
282286

283287
fetchOptimistic(

src/core/tests/queryClient.test.tsx

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { sleep, queryKey, mockConsoleError } from '../../react/tests/utils'
2-
import { QueryCache, QueryClient, QueryFunction, QueryObserver } from '../..'
2+
import {
3+
InfiniteQueryObserver,
4+
QueryCache,
5+
QueryClient,
6+
QueryFunction,
7+
QueryObserver,
8+
} from '../..'
39

410
describe('queryClient', () => {
511
let queryClient: QueryClient
@@ -918,4 +924,96 @@ describe('queryClient', () => {
918924
expect(queryFn2).toHaveBeenCalledTimes(0)
919925
})
920926
})
927+
928+
describe('refetch only certain pages of an infinite query', () => {
929+
test('refetchQueries', async () => {
930+
const key = queryKey()
931+
let multiplier = 1
932+
const observer = new InfiniteQueryObserver<number>(queryClient, {
933+
queryKey: key,
934+
queryFn: ({ pageParam = 10 }) => Number(pageParam) * multiplier,
935+
getNextPageParam: lastPage => lastPage + 1,
936+
})
937+
938+
await observer.fetchNextPage()
939+
await observer.fetchNextPage()
940+
941+
expect(queryClient.getQueryData(key)).toMatchObject({
942+
pages: [10, 11],
943+
})
944+
945+
multiplier = 2
946+
947+
await queryClient.refetchQueries({
948+
queryKey: key,
949+
refetchPage: (_, index) => index === 0,
950+
})
951+
952+
expect(queryClient.getQueryData(key)).toMatchObject({
953+
pages: [20, 11],
954+
})
955+
})
956+
test('invalidateQueries', async () => {
957+
const key = queryKey()
958+
let multiplier = 1
959+
const observer = new InfiniteQueryObserver<number>(queryClient, {
960+
queryKey: key,
961+
queryFn: ({ pageParam = 10 }) => Number(pageParam) * multiplier,
962+
getNextPageParam: lastPage => lastPage + 1,
963+
})
964+
965+
await observer.fetchNextPage()
966+
await observer.fetchNextPage()
967+
968+
expect(queryClient.getQueryData(key)).toMatchObject({
969+
pages: [10, 11],
970+
})
971+
972+
multiplier = 2
973+
974+
await queryClient.invalidateQueries({
975+
queryKey: key,
976+
refetchInactive: true,
977+
refetchPage: (page, _, allPages) => {
978+
return page === allPages[0]
979+
},
980+
})
981+
982+
expect(queryClient.getQueryData(key)).toMatchObject({
983+
pages: [20, 11],
984+
})
985+
})
986+
987+
test('resetQueries', async () => {
988+
const key = queryKey()
989+
let multiplier = 1
990+
new InfiniteQueryObserver<number>(queryClient, {
991+
queryKey: key,
992+
queryFn: ({ pageParam = 10 }) => Number(pageParam) * multiplier,
993+
getNextPageParam: lastPage => lastPage + 1,
994+
initialData: () => ({
995+
pages: [10, 11],
996+
pageParams: [10, 11],
997+
}),
998+
})
999+
1000+
expect(queryClient.getQueryData(key)).toMatchObject({
1001+
pages: [10, 11],
1002+
})
1003+
1004+
multiplier = 2
1005+
1006+
await queryClient.resetQueries({
1007+
queryKey: key,
1008+
inactive: true,
1009+
refetchPage: (page, _, allPages) => {
1010+
return page === allPages[0]
1011+
},
1012+
})
1013+
1014+
expect(queryClient.getQueryData(key)).toMatchObject({
1015+
pages: [20, 11],
1016+
})
1017+
})
1018+
})
9211019
})

0 commit comments

Comments
 (0)