Skip to content

Commit edec4a6

Browse files
authored
feat(core): ensureInfiniteQueryData (#8048)
* feat(core): ensureInfiniteQueryData * docs: ensureInfiniteQueryData * feat(types): make sure we can't pass infiniteQueryOptions to non-infinite query functions like fetchQuery
1 parent 9608f80 commit edec4a6

File tree

5 files changed

+163
-9
lines changed

5 files changed

+163
-9
lines changed

docs/reference/QueryClient.md

+26
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Its available methods are:
2929
- [`queryClient.prefetchInfiniteQuery`](#queryclientprefetchinfinitequery)
3030
- [`queryClient.getQueryData`](#queryclientgetquerydata)
3131
- [`queryClient.ensureQueryData`](#queryclientensurequerydata)
32+
- [`queryClient.ensureInfiniteQueryData`](#queryclientensureinfinitequerydata)
3233
- [`queryClient.getQueriesData`](#queryclientgetqueriesdata)
3334
- [`queryClient.setQueryData`](#queryclientsetquerydata)
3435
- [`queryClient.getQueryState`](#queryclientgetquerystate)
@@ -200,6 +201,31 @@ const data = await queryClient.ensureQueryData({ queryKey, queryFn })
200201

201202
- `Promise<TData>`
202203

204+
## `queryClient.ensureInfiniteQueryData`
205+
206+
`ensureInfiniteQueryData` is an asynchronous function that can be used to get an existing infinite query's cached data. If the query does not exist, `queryClient.fetchInfiniteQuery` will be called and its results returned.
207+
208+
```tsx
209+
const data = await queryClient.ensureInfiniteQueryData({
210+
queryKey,
211+
queryFn,
212+
initialPageParam,
213+
getNextPageParam,
214+
})
215+
```
216+
217+
**Options**
218+
219+
- the same options as [`fetchInfiniteQuery`](#queryclientfetchinfinitequery)
220+
- `revalidateIfStale: boolean`
221+
- Optional
222+
- Defaults to `false`
223+
- If set to `true`, stale data will be refetched in the background, but cached data will be returned immediately.
224+
225+
**Returns**
226+
227+
- `Promise<InfiniteData<TData, TPageParam>>`
228+
203229
## `queryClient.getQueriesData`
204230

205231
`getQueriesData` is a synchronous function that can be used to get the cached data of multiple queries. Only queries that match the passed queryKey or queryFilter will be returned. If there are no matching queries, an empty array will be returned.

packages/query-core/src/__tests__/queryClient.test.tsx

+63
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,69 @@ describe('queryClient', () => {
479479
})
480480
})
481481

482+
describe('ensureInfiniteQueryData', () => {
483+
test('should return the cached query data if the query is found', async () => {
484+
const key = queryKey()
485+
const queryFn = () => Promise.resolve('data')
486+
487+
queryClient.setQueryData([key, 'id'], { pages: ['bar'], pageParams: [0] })
488+
489+
await expect(
490+
queryClient.ensureInfiniteQueryData({
491+
queryKey: [key, 'id'],
492+
queryFn,
493+
initialPageParam: 1,
494+
getNextPageParam: () => undefined,
495+
}),
496+
).resolves.toEqual({ pages: ['bar'], pageParams: [0] })
497+
})
498+
499+
test('should fetch the query and return its results if the query is not found', async () => {
500+
const key = queryKey()
501+
const queryFn = () => Promise.resolve('data')
502+
503+
await expect(
504+
queryClient.ensureInfiniteQueryData({
505+
queryKey: [key, 'id'],
506+
queryFn,
507+
initialPageParam: 1,
508+
getNextPageParam: () => undefined,
509+
}),
510+
).resolves.toEqual({ pages: ['data'], pageParams: [1] })
511+
})
512+
513+
test('should return the cached query data if the query is found and preFetchQuery in the background when revalidateIfStale is set', async () => {
514+
const TIMEOUT = 10
515+
const key = queryKey()
516+
queryClient.setQueryData([key, 'id'], { pages: ['old'], pageParams: [0] })
517+
518+
const queryFn = () =>
519+
new Promise((resolve) => {
520+
setTimeout(() => resolve('new'), TIMEOUT)
521+
})
522+
523+
await expect(
524+
queryClient.ensureInfiniteQueryData({
525+
queryKey: [key, 'id'],
526+
queryFn,
527+
initialPageParam: 1,
528+
getNextPageParam: () => undefined,
529+
revalidateIfStale: true,
530+
}),
531+
).resolves.toEqual({ pages: ['old'], pageParams: [0] })
532+
await sleep(TIMEOUT + 10)
533+
await expect(
534+
queryClient.ensureInfiniteQueryData({
535+
queryKey: [key, 'id'],
536+
queryFn,
537+
initialPageParam: 1,
538+
getNextPageParam: () => undefined,
539+
revalidateIfStale: true,
540+
}),
541+
).resolves.toEqual({ pages: ['new'], pageParams: [0] })
542+
})
543+
})
544+
482545
describe('getQueriesData', () => {
483546
test('should return the query data for all matched queries', () => {
484547
const key1 = queryKey()

packages/query-core/src/queryClient.ts

+28-2
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ import { focusManager } from './focusManager'
1313
import { onlineManager } from './onlineManager'
1414
import { notifyManager } from './notifyManager'
1515
import { infiniteQueryBehavior } from './infiniteQueryBehavior'
16-
import type { QueryState } from './query'
1716
import type {
1817
CancelOptions,
1918
DataTag,
2019
DefaultError,
2120
DefaultOptions,
2221
DefaultedQueryObserverOptions,
22+
EnsureInfiniteQueryDataOptions,
2323
EnsureQueryDataOptions,
2424
FetchInfiniteQueryOptions,
2525
FetchQueryOptions,
@@ -40,6 +40,7 @@ import type {
4040
ResetOptions,
4141
SetDataOptions,
4242
} from './types'
43+
import type { QueryState } from './query'
4344
import type { MutationFilters, QueryFilters, Updater } from './utils'
4445

4546
// TYPES
@@ -385,7 +386,7 @@ export class QueryClient {
385386
TData,
386387
TPageParam
387388
>(options.pages)
388-
return this.fetchQuery(options)
389+
return this.fetchQuery(options as any)
389390
}
390391

391392
prefetchInfiniteQuery<
@@ -406,6 +407,31 @@ export class QueryClient {
406407
return this.fetchInfiniteQuery(options).then(noop).catch(noop)
407408
}
408409

410+
ensureInfiniteQueryData<
411+
TQueryFnData,
412+
TError = DefaultError,
413+
TData = TQueryFnData,
414+
TQueryKey extends QueryKey = QueryKey,
415+
TPageParam = unknown,
416+
>(
417+
options: EnsureInfiniteQueryDataOptions<
418+
TQueryFnData,
419+
TError,
420+
TData,
421+
TQueryKey,
422+
TPageParam
423+
>,
424+
): Promise<InfiniteData<TData, TPageParam>> {
425+
options.behavior = infiniteQueryBehavior<
426+
TQueryFnData,
427+
TError,
428+
TData,
429+
TPageParam
430+
>(options.pages)
431+
432+
return this.ensureQueryData(options as any)
433+
}
434+
409435
resumePausedMutations(): Promise<unknown> {
410436
if (onlineManager.isOnline()) {
411437
return this.#mutationCache.resumePausedMutations()

packages/query-core/src/types.ts

+26-6
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@ export interface FetchQueryOptions<
448448
QueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
449449
'queryKey'
450450
> {
451+
initialPageParam?: never
451452
/**
452453
* The time in milliseconds after data is considered stale.
453454
* If the data is fresh it will be returned from the cache.
@@ -471,6 +472,22 @@ export interface EnsureQueryDataOptions<
471472
revalidateIfStale?: boolean
472473
}
473474

475+
export type EnsureInfiniteQueryDataOptions<
476+
TQueryFnData = unknown,
477+
TError = DefaultError,
478+
TData = TQueryFnData,
479+
TQueryKey extends QueryKey = QueryKey,
480+
TPageParam = unknown,
481+
> = FetchInfiniteQueryOptions<
482+
TQueryFnData,
483+
TError,
484+
TData,
485+
TQueryKey,
486+
TPageParam
487+
> & {
488+
revalidateIfStale?: boolean
489+
}
490+
474491
type FetchInfiniteQueryPages<TQueryFnData = unknown, TPageParam = unknown> =
475492
| { pages?: never }
476493
| {
@@ -484,12 +501,15 @@ export type FetchInfiniteQueryOptions<
484501
TData = TQueryFnData,
485502
TQueryKey extends QueryKey = QueryKey,
486503
TPageParam = unknown,
487-
> = FetchQueryOptions<
488-
TQueryFnData,
489-
TError,
490-
InfiniteData<TData, TPageParam>,
491-
TQueryKey,
492-
TPageParam
504+
> = Omit<
505+
FetchQueryOptions<
506+
TQueryFnData,
507+
TError,
508+
InfiniteData<TData, TPageParam>,
509+
TQueryKey,
510+
TPageParam
511+
>,
512+
'initialPageParam'
493513
> &
494514
InitialPageParam<TPageParam> &
495515
FetchInfiniteQueryPages<TQueryFnData, TPageParam>

packages/react-query/src/__tests__/infiniteQueryOptions.test-d.tsx

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { describe, expectTypeOf, it } from 'vitest'
1+
import { describe, expectTypeOf, it, test } from 'vitest'
22
import { QueryClient, dataTagSymbol } from '@tanstack/query-core'
33
import { infiniteQueryOptions } from '../infiniteQueryOptions'
44
import { useInfiniteQuery } from '../useInfiniteQuery'
55
import { useSuspenseInfiniteQuery } from '../useSuspenseInfiniteQuery'
6+
import { useQuery } from '../useQuery'
67
import type { InfiniteData } from '@tanstack/query-core'
78

89
describe('queryOptions', () => {
@@ -133,4 +134,22 @@ describe('queryOptions', () => {
133134
InfiniteData<string, unknown> | undefined
134135
>()
135136
})
137+
138+
test('should not be allowed to be passed to non-infinite query functions', () => {
139+
const queryClient = new QueryClient()
140+
const options = infiniteQueryOptions({
141+
queryKey: ['key'],
142+
queryFn: () => Promise.resolve('string'),
143+
getNextPageParam: () => 1,
144+
initialPageParam: 1,
145+
})
146+
// @ts-expect-error cannot pass infinite options to non-infinite query functions
147+
useQuery(options)
148+
// @ts-expect-error cannot pass infinite options to non-infinite query functions
149+
queryClient.ensureQueryData(options)
150+
// @ts-expect-error cannot pass infinite options to non-infinite query functions
151+
queryClient.fetchQuery(options)
152+
// @ts-expect-error cannot pass infinite options to non-infinite query functions
153+
queryClient.prefetchQuery(options)
154+
})
136155
})

0 commit comments

Comments
 (0)