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(react-query): Add usePrefetchQueries hook #8734

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
4 changes: 4 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,10 @@
"label": "usePrefetchQuery",
"to": "framework/react/reference/usePrefetchQuery"
},
{
"label": "usePrefetchQueries",
"to": "framework/react/reference/usePrefetchQueries"
},
{
"label": "usePrefetchInfiniteQuery",
"to": "framework/react/reference/usePrefetchInfiniteQuery"
Expand Down
18 changes: 9 additions & 9 deletions docs/framework/react/guides/prefetching.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ There are a few different prefetching patterns:
3. Via router integration
4. During Server Rendering (another form of router integration)

In this guide, we'll take a look at the first three, while the fourth will be covered in depth in the [Server Rendering & Hydration guide](../ssr) and the [Advanced Server Rendering guide](../advanced-ssr).
In this guide, we'll take a look at the first three, while the fourth will be covered in depth in the [Server Rendering & Hydration guide](../guides/ssr) and the [Advanced Server Rendering guide](../guides/advanced-ssr).

One specific use of prefetching is to avoid Request Waterfalls, for an in-depth background and explanation of those, see the [Performance & Request Waterfalls guide](../request-waterfalls).
One specific use of prefetching is to avoid Request Waterfalls, for an in-depth background and explanation of those, see the [Performance & Request Waterfalls guide](../guides/request-waterfalls).

## prefetchQuery & prefetchInfiniteQuery

Expand Down Expand Up @@ -196,7 +196,7 @@ 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. For this scenario, you can use the [`usePrefetchQuery`](../../reference/usePrefetchQuery) or the [`usePrefetchInfiniteQuery`](../../reference/usePrefetchInfiniteQuery) hooks available in the library.
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), [`usePrefetchQueries`](../reference/usePrefetchQueries) 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.

Expand Down Expand Up @@ -256,7 +256,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
- Prefetch before a suspense boundary using `usePrefetchQuery`, `usePrefetchQueries` or `usePrefetchInfiniteQuery` hooks
- Use `useQuery` or `useSuspenseQueries` and ignore the result
- Prefetch inside the query function
- Prefetch in an effect
Expand All @@ -267,7 +267,7 @@ Let's look at a slightly more advanced case next.

### Dependent Queries & Code Splitting

Sometimes we want to prefetch conditionally, based on the result of another fetch. Consider this example borrowed from the [Performance & Request Waterfalls guide](../request-waterfalls):
Sometimes we want to prefetch conditionally, based on the result of another fetch. Consider this example borrowed from the [Performance & Request Waterfalls guide](../guides/request-waterfalls):

[//]: # 'ExampleConditionally1'

Expand Down Expand Up @@ -367,7 +367,7 @@ There is a tradeoff however, in that the code for `getGraphDataById` is now incl

Because data fetching in the component tree itself can easily lead to request waterfalls and the different fixes for that can be cumbersome as they accumulate throughout the application, an attractive way to do prefetching is integrating it at the router level.

In this approach, you explicitly declare for each _route_ what data is going to be needed for that component tree, ahead of time. Because Server Rendering has traditionally needed all data to be loaded before rendering starts, this has been the dominating approach for SSR'd apps for a long time. This is still a common approach and you can read more about it in the [Server Rendering & Hydration guide](../ssr).
In this approach, you explicitly declare for each _route_ what data is going to be needed for that component tree, ahead of time. Because Server Rendering has traditionally needed all data to be loaded before rendering starts, this has been the dominating approach for SSR'd apps for a long time. This is still a common approach and you can read more about it in the [Server Rendering & Hydration guide](../guides/ssr).

For now, let's focus on the client side case and look at an example of how you can make this work with [Tanstack Router](https://tanstack.com/router). These examples leave out a lot of setup and boilerplate to stay concise, you can check out a [full React Query example](https://tanstack.com/router/v1/docs/framework/react/examples/basic-react-query-file-based) over in the [Tanstack Router docs](https://tanstack.com/router/v1/docs).

Expand Down Expand Up @@ -412,13 +412,13 @@ const articleRoute = new Route({
})
```

Integration with other routers is also possible, see the [React Router example](../../examples/react-router) for another demonstration.
Integration with other routers is also possible, see the [React Router example](../examples/react-router) for another demonstration.

[//]: # 'Router'

## Manually Priming a Query

If you already have the data for your query synchronously available, you don't need to prefetch it. You can just use the [Query Client's `setQueryData` method](../../../../reference/QueryClient/#queryclientsetquerydata) to directly add or update a query's cached result by key.
If you already have the data for your query synchronously available, you don't need to prefetch it. You can just use the [Query Client's `setQueryData` method](../../../reference/QueryClient/#queryclientsetquerydata) to directly add or update a query's cached result by key.

[//]: # 'ExampleManualPriming'

Expand All @@ -433,6 +433,6 @@ queryClient.setQueryData(['todos'], todos)

For a deep-dive on how to get data into your Query Cache before you fetch, have a look at [#17: Seeding the Query Cache](../community/tkdodos-blog#17-seeding-the-query-cache) from the Community Resources.

Integrating with Server Side routers and frameworks is very similar to what we just saw, with the addition that the data has to passed from the server to the client to be hydrated into the cache there. To learn how, continue on to the [Server Rendering & Hydration guide](../ssr).
Integrating with Server Side routers and frameworks is very similar to what we just saw, with the addition that the data has to passed from the server to the client to be hydrated into the cache there. To learn how, continue on to the [Server Rendering & Hydration guide](../guides/ssr).

[//]: # 'Materials'
40 changes: 40 additions & 0 deletions docs/framework/react/reference/usePrefetchQueries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
id: usePrefetchQueries
title: usePrefetchQueries
---

```tsx
const ids = [1, 2, 3]

const queryOpts = ids.map((id) => ({
queryKey: ['post', id],
queryFn: () => fetchPost(id),
staleTime: Infinity,
}))

// parent component
usePrefetchQueries({
queries: queryOps,
})

// child component with suspense
const results = useSuspenseQueries({
queries: queryOpts,
})
```

**Options**

The `useQueries` hook accepts an options object with a **queries** key whose value is an array with query option objects identical to the [`usePrefetchQuery` hook](../reference/usePrefetchQuery). 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/useSuspenseQueries).
68 changes: 68 additions & 0 deletions packages/react-query/src/__tests__/usePrefetchQueries.test-d.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { describe, expectTypeOf, it } from 'vitest'
import { usePrefetchQueries } from '..'

describe('usePrefetchQueries', () => {
it('should return nothing', () => {
const result = usePrefetchQueries({
queries: [
{
queryKey: ['key1'],
queryFn: () => Promise.resolve(5),
},
{
queryKey: ['key2'],
queryFn: () => Promise.resolve('data'),
},
{
queryKey: ['key3'],
queryFn: () =>
Promise.resolve({
foo: 1,
bar: 'fizzbuzz',
}),
},
],
})

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

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

usePrefetchQueries({
queries: [
{
queryKey: ['key1'],
queryFn: () => Promise.resolve('data'),
// @ts-expect-error TS2345
enabled: true,
},
],
})

usePrefetchQueries({
queries: [
{
queryKey: ['key1'],
queryFn: () =>
Promise.resolve({
foo: 1,
bar: 'fizzbuzz',
}),
// @ts-expect-error TS2345
throwOnError: true,
},
],
})
})
})
196 changes: 196 additions & 0 deletions packages/react-query/src/__tests__/usePrefetchQueries.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { describe, expect, it, vi } from 'vitest'
import React from 'react'
import { waitFor } from '@testing-library/react'
import { QueryCache, usePrefetchQueries, useSuspenseQueries } from '..'
import { createQueryClient, queryKey, renderWithClient, sleep } from './utils'

import type { UseSuspenseQueryOptions } from '..'

const generateQueryFn = (data: string) =>
vi
.fn<(...args: Array<any>) => Promise<string>>()
.mockImplementation(async () => {
await sleep(10)

return data
})

describe('usePrefetchQueries', () => {
const queryCache = new QueryCache()
const queryClient = createQueryClient({ queryCache })

function Suspended<TData = unknown>(props: {
queriesOpts: Array<
UseSuspenseQueryOptions<TData, Error, TData, Array<string>>
>
children?: React.ReactNode
}) {
const state = useSuspenseQueries({
queries: props.queriesOpts,
combine: (results) => results.map((r) => r.data),
})

return (
<div>
<div>data: {state.map((data) => String(data)).join(', ')}</div>
{props.children}
</div>
)
}

it('should prefetch queries if query states do not exist', async () => {
const queryOpts1 = {
queryKey: queryKey(),
queryFn: generateQueryFn('prefetchQuery1'),
}

const queryOpts2 = {
queryKey: queryKey(),
queryFn: generateQueryFn('prefetchQuery2'),
}

const componentQueryOpts1 = {
...queryOpts1,
queryFn: generateQueryFn('useSuspenseQuery1'),
}

const componentQueryOpts2 = {
...queryOpts2,
queryFn: generateQueryFn('useSuspenseQuery2'),
}

function App() {
usePrefetchQueries({
queries: [queryOpts1, queryOpts2],
})

return (
<React.Suspense fallback="Loading...">
<Suspended queriesOpts={[componentQueryOpts1, componentQueryOpts2]} />
</React.Suspense>
)
}

const rendered = renderWithClient(queryClient, <App />)

await waitFor(() =>
rendered.getByText('data: prefetchQuery1, prefetchQuery2'),
)
expect(queryOpts1.queryFn).toHaveBeenCalledTimes(1)
expect(queryOpts2.queryFn).toHaveBeenCalledTimes(1)
})

it('should not prefetch queries if query states exist', async () => {
const queryOpts1 = {
queryKey: queryKey(),
queryFn: generateQueryFn('The usePrefetchQueries hook is smart! 1'),
}

const queryOpts2 = {
queryKey: queryKey(),
queryFn: generateQueryFn('The usePrefetchQueries hook is smart! 2'),
}

function App() {
usePrefetchQueries({
queries: [queryOpts1, queryOpts2],
})

return (
<React.Suspense fallback="Loading...">
<Suspended queriesOpts={[queryOpts1, queryOpts2]} />
</React.Suspense>
)
}

await queryClient.fetchQuery(queryOpts1)
await queryClient.fetchQuery(queryOpts2)
queryOpts1.queryFn.mockClear()
queryOpts2.queryFn.mockClear()

const rendered = renderWithClient(queryClient, <App />)

expect(rendered.queryByText('fetching: true')).not.toBeInTheDocument()
await waitFor(() =>
rendered.getByText(
'data: The usePrefetchQueries hook is smart! 1, The usePrefetchQueries hook is smart! 2',
),
)
expect(queryOpts1.queryFn).not.toHaveBeenCalled()
expect(queryOpts2.queryFn).not.toHaveBeenCalled()
})

it('should only prefetch queries that do not exist', async () => {
const queryOpts1 = {
queryKey: queryKey(),
queryFn: generateQueryFn('The usePrefetchQueries hook is smart! 1'),
}

const queryOpts2 = {
queryKey: queryKey(),
queryFn: generateQueryFn('The usePrefetchQueries hook is smart! 2'),
}

function App() {
usePrefetchQueries({
queries: [queryOpts1, queryOpts2],
})

return (
<React.Suspense fallback="Loading...">
<Suspended queriesOpts={[queryOpts1, queryOpts2]} />
</React.Suspense>
)
}

await queryClient.fetchQuery(queryOpts1)
queryOpts1.queryFn.mockClear()
queryOpts2.queryFn.mockClear()

const rendered = renderWithClient(queryClient, <App />)

await waitFor(() =>
rendered.getByText(
'data: The usePrefetchQueries hook is smart! 1, The usePrefetchQueries hook is smart! 2',
),
)
expect(queryOpts1.queryFn).not.toHaveBeenCalled()
expect(queryOpts2.queryFn).toHaveBeenCalledTimes(1)
})

it('should not create an endless loop when using inside a suspense boundary', async () => {
const queryOpts1 = {
queryKey: queryKey(),
queryFn: generateQueryFn('prefetchedQuery1'),
}

const queryOpts2 = {
queryKey: queryKey(),
queryFn: generateQueryFn('prefetchedQuery2'),
}

function Prefetch({ children }: { children: React.ReactNode }) {
usePrefetchQueries({
queries: [queryOpts1, queryOpts2],
})
return <>{children}</>
}

function App() {
return (
<React.Suspense>
<Prefetch>
<Suspended queriesOpts={[queryOpts1, queryOpts2]} />
</Prefetch>
</React.Suspense>
)
}

const rendered = renderWithClient(queryClient, <App />)
await waitFor(() =>
rendered.getByText('data: prefetchedQuery1, prefetchedQuery2'),
)
expect(queryOpts1.queryFn).toHaveBeenCalledTimes(1)
expect(queryOpts2.queryFn).toHaveBeenCalledTimes(1)
})
})
1 change: 1 addition & 0 deletions packages/react-query/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type {
SuspenseQueriesOptions,
} from './useSuspenseQueries'
export { usePrefetchQuery } from './usePrefetchQuery'
export { usePrefetchQueries } from './usePrefetchQueries'
export { usePrefetchInfiniteQuery } from './usePrefetchInfiniteQuery'
export { queryOptions } from './queryOptions'
export type {
Expand Down
Loading
Loading