Skip to content

Commit fbfe940

Browse files
TkDodobrunolopesrEphem
authoredJun 25, 2024··
feat(react-query): usePrefetchQuery (#7582)
* feat: usePrefetchQuery * refactor: switch to actual prefetching * refactor: remove ensureInfiniteQueryData function will do in a separate PR * chore: add tests for usePrefetchQuery and usePrefetchInfiniteQuery (#7586) * chore: add tests for usePrefetchQuery and usePrefetchInfiniteQuery * chore: update tests to assert the alternative spy is not called * chore: add some new tests * chore: remove it.only whoops * chore: call mockClear after fetching * chore: improve waterfall test by asserting fallback calls instead of loading node query * chore: improve code repetition * chore: add some generics to helper functions * usePrefetchQuery type tests and docs (#7592) * chore: add type tests and docs * chore: update hooks to use FetchQueryOptions and FetchInfiniteQueryOptions * chore: update tests * chore: update docs * chore: remove .md extension from link * chore: add unknown default value to TQueryFnData * Apply suggestions from code review --------- Co-authored-by: Dominik Dorfmeister <office@dorfmeister.cc> * Apply suggestions from code review Co-authored-by: Fredrik Höglund <fredrik.hoglund@gmail.com> * chore: fix types in tests * chore: add new tests (#7614) * chore: add new tests * Apply suggestions from code review --------- Co-authored-by: Dominik Dorfmeister <office@dorfmeister.cc> --------- Co-authored-by: Bruno Lopes <88719327+brunolopesr@users.noreply.github.com> Co-authored-by: Fredrik Höglund <fredrik.hoglund@gmail.com>
1 parent 6355244 commit fbfe940

File tree

8 files changed

+656
-33
lines changed

8 files changed

+656
-33
lines changed
 

‎docs/config.json

+8
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,14 @@
658658
"label": "infiniteQueryOptions",
659659
"to": "framework/react/reference/infiniteQueryOptions"
660660
},
661+
{
662+
"label": "usePrefetchQuery",
663+
"to": "framework/react/reference/usePrefetchQuery"
664+
},
665+
{
666+
"label": "usePrefetchInfiniteQuery",
667+
"to": "framework/react/reference/usePrefetchInfiniteQuery"
668+
},
661669
{
662670
"label": "QueryErrorResetBoundary",
663671
"to": "framework/react/reference/QueryErrorResetBoundary"

‎docs/framework/react/guides/prefetching.md

+30-33
Original file line numberDiff line numberDiff line change
@@ -196,45 +196,41 @@ This starts fetching `'article-comments'` immediately and flattens the waterfall
196196

197197
[//]: # 'Suspense'
198198

199-
If you want to prefetch together with Suspense, you will have to do things a bit differently. You can't use `useSuspenseQueries` to prefetch, since the prefetch would block the component from rendering. You also can not use `useQuery` for the prefetch, because that wouldn't start the prefetch until after suspenseful query had resolved. What you can do is add a small `usePrefetchQuery` function (we might add this to the library itself at a later point):
200-
201-
```tsx
202-
function usePrefetchQuery(options) {
203-
const queryClient = useQueryClient()
204-
205-
// This happens in render, but is safe to do because ensureQueryData
206-
// only fetches if there is no data in the cache for this query. This
207-
// means we know no observers are watching the data so the side effect
208-
// is not observable, which is safe.
209-
if (!queryClient.getQueryState(options.queryKey)) {
210-
queryClient.ensureQueryData(options).catch(() => {
211-
// Avoid uncaught error
212-
})
213-
}
214-
}
215-
```
216-
217-
This approach works with both `useQuery` and `useSuspenseQuery`, so feel free to use it as an alternative to the `useQuery({ ..., notifyOnChangeProps: [] })` approach as well. The only tradeoff is that the above function will never fetch and _update_ existing data in the cache if it's stale, but this will usually happen in the later query anyway.
199+
If you want to prefetch together with Suspense, you will have to do things a bit differently. You can't use `useSuspenseQueries` to prefetch, since the prefetch would block the component from rendering. You also can not use `useQuery` for the prefetch, because that wouldn't start the prefetch until after suspenseful query had resolved. For this scenario, you can use the [`usePrefetchQuery`](../reference/usePrefetchQuery) or the [`usePrefetchInfiniteQuery`](../reference/usePrefetchInfiniteQuery) hooks available in the library.
218200

219201
You can now use `useSuspenseQuery` in the component that actually needs the data. You _might_ want to wrap this later component in its own `<Suspense>` boundary so the "secondary" query we are prefetching does not block rendering of the "primary" data.
220202

221203
```tsx
222-
// Prefetch
223-
usePrefetchQuery({
224-
queryKey: ['article-comments', id],
225-
queryFn: getArticleCommentsById,
226-
})
204+
function App() {
205+
usePrefetchQuery({
206+
queryKey: ['articles'],
207+
queryFn: (...args) => {
208+
return getArticles(...args)
209+
},
210+
})
227211

228-
const { data: articleResult } = useSuspenseQuery({
229-
queryKey: ['article', id],
230-
queryFn: getArticleById,
231-
})
212+
return (
213+
<Suspense fallback="Loading articles...">
214+
<Articles />
215+
</Suspense>
216+
)
217+
}
232218

233-
// In nested component:
234-
const { data: commentsResult } = useSuspenseQuery({
235-
queryKey: ['article-comments', id],
236-
queryFn: getArticleCommentsById,
237-
})
219+
function Articles() {
220+
const { data: articles } = useSuspenseQuery({
221+
queryKey: ['articles'],
222+
queryFn: (...args) => {
223+
return getArticles(...args)
224+
},
225+
})
226+
227+
return articles.map((article) => (
228+
<div key={articleData.id}>
229+
<ArticleHeader article={article} />
230+
<ArticleBody article={article} />
231+
</div>
232+
))
233+
}
238234
```
239235

240236
Another way is to prefetch inside of the query function. This makes sense if you know that every time an article is fetched it's very likely comments will also be needed. For this, we'll use `queryClient.prefetchQuery`:
@@ -269,6 +265,7 @@ useEffect(() => {
269265

270266
To recap, if you want to prefetch a query during the component lifecycle, there are a few different ways to do it, pick the one that suits your situation best:
271267

268+
- Prefetch before a suspense boundary using `usePrefetchQuery` or `usePrefetchInfiniteQuery` hooks
272269
- Use `useQuery` or `useSuspenseQueries` and ignore the result
273270
- Prefetch inside the query function
274271
- Prefetch in an effect
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
id: usePrefetchInfiniteQuery
3+
title: usePrefetchInfiniteQuery
4+
---
5+
6+
```tsx
7+
const result = usePrefetchInfiniteQuery(options)
8+
```
9+
10+
**Options**
11+
12+
You can pass everything to `usePrefetchInfiniteQuery` that you can pass to [`queryClient.prefetchInfiniteQuery`](../../../reference/QueryClient#queryclientprefetchinfinitequery). Remember that some of them are required as below:
13+
14+
- `queryKey: QueryKey`
15+
16+
- **Required**
17+
- The query key to prefetch during render
18+
19+
- `queryFn: (context: QueryFunctionContext) => Promise<TData>`
20+
21+
- **Required, but only if no default query function has been defined** See [Default Query Function](../../guides/default-query-function) for more information.
22+
23+
- `initialPageParam: TPageParam`
24+
25+
- **Required**
26+
- The default page param to use when fetching the first page.
27+
28+
- `getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => TPageParam | undefined | null`
29+
30+
- **Required**
31+
- When new data is received for this query, this function receives both the last page of the infinite list of data and the full array of all pages, as well as pageParam information.
32+
- It should return a **single variable** that will be passed as the last optional parameter to your query function.
33+
- Return `undefined` or `null` to indicate there is no next page available.
34+
35+
- **Returns**
36+
37+
The `usePrefetchInfiniteQuery` does not return anything, it should be used just to fire a prefetch during render, before a suspense boundary that wraps a component that uses [`useSuspenseInfiniteQuery`](../reference/useSuspenseInfiniteQuery)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
id: usePrefetchQuery
3+
title: usePrefetchQuery
4+
---
5+
6+
```tsx
7+
const result = usePrefetchQuery(options)
8+
```
9+
10+
**Options**
11+
12+
You can pass everything to `usePrefetchQuery` that you can pass to [`queryClient.prefetchQuery`](../../../reference/QueryClient#queryclientprefetchquery). Remember that some of them are required as below:
13+
14+
- `queryKey: QueryKey`
15+
16+
- **Required**
17+
- The query key to prefetch during render
18+
19+
- `queryFn: (context: QueryFunctionContext) => Promise<TData>`
20+
- **Required, but only if no default query function has been defined** See [Default Query Function](../../guides/default-query-function) for more information.
21+
22+
**Returns**
23+
24+
The `usePrefetchQuery` does not return anything, it should be used just to fire a prefetch during render, before a suspense boundary that wraps a component that uses [`useSuspenseQuery`](../reference/useSuspenseQuery).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { describe, expectTypeOf, it } from 'vitest'
2+
import { usePrefetchInfiniteQuery, usePrefetchQuery } from '../prefetch'
3+
4+
describe('usePrefetchQuery', () => {
5+
it('should return nothing', () => {
6+
const result = usePrefetchQuery({
7+
queryKey: ['key'],
8+
queryFn: () => Promise.resolve(5),
9+
})
10+
11+
expectTypeOf(result).toEqualTypeOf<void>()
12+
})
13+
14+
it('should not allow refetchInterval, enabled or throwOnError options', () => {
15+
usePrefetchQuery({
16+
queryKey: ['key'],
17+
queryFn: () => Promise.resolve(5),
18+
// @ts-expect-error TS2345
19+
refetchInterval: 1000,
20+
})
21+
22+
usePrefetchQuery({
23+
queryKey: ['key'],
24+
queryFn: () => Promise.resolve(5),
25+
// @ts-expect-error TS2345
26+
enabled: true,
27+
})
28+
29+
usePrefetchQuery({
30+
queryKey: ['key'],
31+
queryFn: () => Promise.resolve(5),
32+
// @ts-expect-error TS2345
33+
throwOnError: true,
34+
})
35+
})
36+
})
37+
38+
describe('useInfinitePrefetchQuery', () => {
39+
it('should return nothing', () => {
40+
const result = usePrefetchInfiniteQuery({
41+
queryKey: ['key'],
42+
queryFn: () => Promise.resolve(5),
43+
initialPageParam: 1,
44+
getNextPageParam: () => 1,
45+
})
46+
47+
expectTypeOf(result).toEqualTypeOf<void>()
48+
})
49+
50+
it('should require initialPageParam and getNextPageParam', () => {
51+
// @ts-expect-error TS2345
52+
usePrefetchInfiniteQuery({
53+
queryKey: ['key'],
54+
queryFn: () => Promise.resolve(5),
55+
})
56+
})
57+
58+
it('should not allow refetchInterval, enabled or throwOnError options', () => {
59+
usePrefetchQuery({
60+
queryKey: ['key'],
61+
queryFn: () => Promise.resolve(5),
62+
// @ts-expect-error TS2345
63+
refetchInterval: 1000,
64+
})
65+
66+
usePrefetchQuery({
67+
queryKey: ['key'],
68+
queryFn: () => Promise.resolve(5),
69+
// @ts-expect-error TS2345
70+
enabled: true,
71+
})
72+
73+
usePrefetchQuery({
74+
queryKey: ['key'],
75+
queryFn: () => Promise.resolve(5),
76+
// @ts-expect-error TS2345
77+
throwOnError: true,
78+
})
79+
})
80+
})

‎packages/react-query/src/__tests__/prefetch.test.tsx

+434
Large diffs are not rendered by default.

‎packages/react-query/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type {
1515
SuspenseQueriesResults,
1616
SuspenseQueriesOptions,
1717
} from './useSuspenseQueries'
18+
export { usePrefetchQuery, usePrefetchInfiniteQuery } from './prefetch'
1819
export { queryOptions } from './queryOptions'
1920
export type {
2021
DefinedInitialDataOptions,

‎packages/react-query/src/prefetch.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useQueryClient } from './QueryClientProvider'
2+
import type {
3+
DefaultError,
4+
FetchInfiniteQueryOptions,
5+
FetchQueryOptions,
6+
QueryKey,
7+
} from '@tanstack/query-core'
8+
9+
export function usePrefetchQuery<
10+
TQueryFnData = unknown,
11+
TError = DefaultError,
12+
TData = TQueryFnData,
13+
TQueryKey extends QueryKey = QueryKey,
14+
>(options: FetchQueryOptions<TQueryFnData, TError, TData, TQueryKey>) {
15+
const queryClient = useQueryClient()
16+
17+
if (!queryClient.getQueryState(options.queryKey)) {
18+
queryClient.prefetchQuery(options)
19+
}
20+
}
21+
22+
export function usePrefetchInfiniteQuery<
23+
TQueryFnData = unknown,
24+
TError = DefaultError,
25+
TData = TQueryFnData,
26+
TQueryKey extends QueryKey = QueryKey,
27+
TPageParam = unknown,
28+
>(
29+
options: FetchInfiniteQueryOptions<
30+
TQueryFnData,
31+
TError,
32+
TData,
33+
TQueryKey,
34+
TPageParam
35+
>,
36+
) {
37+
const queryClient = useQueryClient()
38+
39+
if (!queryClient.getQueryState(options.queryKey)) {
40+
queryClient.prefetchInfiniteQuery(options)
41+
}
42+
}

0 commit comments

Comments
 (0)
Please sign in to comment.