Skip to content

Commit

Permalink
feat(react-query): usePrefetchQuery (#7582)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* Apply suggestions from code review

Co-authored-by: Fredrik Höglund <[email protected]>

* chore: fix types in tests

* chore: add new tests (#7614)

* chore: add new tests

* Apply suggestions from code review

---------

Co-authored-by: Dominik Dorfmeister <[email protected]>

---------

Co-authored-by: Bruno Lopes <[email protected]>
Co-authored-by: Fredrik Höglund <[email protected]>
  • Loading branch information
3 people committed Jun 25, 2024
1 parent 6355244 commit fbfe940
Show file tree
Hide file tree
Showing 8 changed files with 656 additions and 33 deletions.
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

0 comments on commit fbfe940

Please sign in to comment.