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

feat: usePrefetchQuery #7582

Merged
merged 12 commits into from
Jun 25, 2024
8 changes: 8 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,14 @@
"label": "infiniteQueryOptions",
"to": "framework/react/reference/infiniteQueryOptions"
},
{
"label": "usePrefetchQuery",
"to": "framework/react/reference/usePrefetchQuery"
},
{
"label": "usePrefetchInfiniteQuery",
"to": "framework/react/reference/usePrefetchInfiniteQuery"
},
{
"label": "QueryErrorResetBoundary",
"to": "framework/react/reference/QueryErrorResetBoundary"
Expand Down
63 changes: 30 additions & 33 deletions docs/framework/react/guides/prefetching.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,45 +196,41 @@ This starts fetching `'article-comments'` immediately and flattens the waterfall

[//]: # 'Suspense'

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):

```tsx
function usePrefetchQuery(options) {
const queryClient = useQueryClient()

// This happens in render, but is safe to do because ensureQueryData
// only fetches if there is no data in the cache for this query. This
// means we know no observers are watching the data so the side effect
// is not observable, which is safe.
if (!queryClient.getQueryState(options.queryKey)) {
queryClient.ensureQueryData(options).catch(() => {
// Avoid uncaught error
})
}
}
```

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.
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.

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.

```tsx
// Prefetch
usePrefetchQuery({
queryKey: ['article-comments', id],
queryFn: getArticleCommentsById,
})
function App() {
usePrefetchQuery({
queryKey: ['articles'],
queryFn: (...args) => {
return getArticles(...args)
},
})

const { data: articleResult } = useSuspenseQuery({
queryKey: ['article', id],
queryFn: getArticleById,
})
return (
<Suspense fallback="Loading articles...">
<Articles />
</Suspense>
)
}

// In nested component:
const { data: commentsResult } = useSuspenseQuery({
queryKey: ['article-comments', id],
queryFn: getArticleCommentsById,
})
function Articles() {
const { data: articles } = useSuspenseQuery({
queryKey: ['articles'],
queryFn: (...args) => {
return getArticles(...args)
},
})

return articles.map((article) => (
<div key={articleData.id}>
<ArticleHeader article={article} />
<ArticleBody article={article} />
</div>
))
}
```

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`:
Expand Down Expand Up @@ -269,6 +265,7 @@ useEffect(() => {

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:

- Prefetch before a suspense boundary using `usePrefetchQuery` or `usePrefetchInfiniteQuery` hooks
- Use `useQuery` or `useSuspenseQueries` and ignore the result
- Prefetch inside the query function
- Prefetch in an effect
Expand Down
37 changes: 37 additions & 0 deletions docs/framework/react/reference/usePrefetchInfiniteQuery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
id: usePrefetchInfiniteQuery
title: usePrefetchInfiniteQuery
---

```tsx
const result = usePrefetchInfiniteQuery(options)
```

**Options**

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:

- `queryKey: QueryKey`

- **Required**
- The query key to prefetch during render

- `queryFn: (context: QueryFunctionContext) => Promise<TData>`

- **Required, but only if no default query function has been defined** See [Default Query Function](../../guides/default-query-function) for more information.

- `initialPageParam: TPageParam`

- **Required**
- The default page param to use when fetching the first page.

- `getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => TPageParam | undefined | null`

- **Required**
- 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.
- It should return a **single variable** that will be passed as the last optional parameter to your query function.
- Return `undefined` or `null` to indicate there is no next page available.

- **Returns**

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)
24 changes: 24 additions & 0 deletions docs/framework/react/reference/usePrefetchQuery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
id: usePrefetchQuery
title: usePrefetchQuery
---

```tsx
const result = usePrefetchQuery(options)
```

**Options**

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:

- `queryKey: QueryKey`

- **Required**
- The query key to prefetch during render

- `queryFn: (context: QueryFunctionContext) => Promise<TData>`
- **Required, but only if no default query function has been defined** See [Default Query Function](../../guides/default-query-function) for more information.

**Returns**

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).
80 changes: 80 additions & 0 deletions packages/react-query/src/__tests__/prefetch.test-d.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { describe, expectTypeOf, it } from 'vitest'
import { usePrefetchInfiniteQuery, usePrefetchQuery } from '../prefetch'

describe('usePrefetchQuery', () => {
it('should return nothing', () => {
const result = usePrefetchQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
})

expectTypeOf(result).toEqualTypeOf<void>()
})

it('should not allow refetchInterval, enabled or throwOnError options', () => {
usePrefetchQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
// @ts-expect-error TS2345
refetchInterval: 1000,
})

usePrefetchQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
// @ts-expect-error TS2345
enabled: true,
})

usePrefetchQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
// @ts-expect-error TS2345
throwOnError: true,
})
})
})

describe('useInfinitePrefetchQuery', () => {
it('should return nothing', () => {
const result = usePrefetchInfiniteQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
initialPageParam: 1,
getNextPageParam: () => 1,
})

expectTypeOf(result).toEqualTypeOf<void>()
})

it('should require initialPageParam and getNextPageParam', () => {
// @ts-expect-error TS2345
usePrefetchInfiniteQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
})
})

it('should not allow refetchInterval, enabled or throwOnError options', () => {
usePrefetchQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
// @ts-expect-error TS2345
refetchInterval: 1000,
})

usePrefetchQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
// @ts-expect-error TS2345
enabled: true,
})

usePrefetchQuery({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
// @ts-expect-error TS2345
throwOnError: true,
})
})
})
Loading
Loading