Skip to content

feat(react-query): Add usePrefetchQueries hook #8734

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

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,10 @@
"label": "usePrefetchQuery",
"to": "framework/react/reference/usePrefetchQuery"
},
{
"label": "usePrefetchQueries",
"to": "framework/react/reference/usePrefetchQueries"
},
{
"label": "usePrefetchInfiniteQuery",
"to": "framework/react/reference/usePrefetchInfiniteQuery"
Expand Down
4 changes: 2 additions & 2 deletions docs/framework/react/guides/prefetching.md
Original file line number Diff line number Diff line change
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.md) or the [`usePrefetchInfiniteQuery`](../reference/usePrefetchInfiniteQuery.md) 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.md), [`usePrefetchQueries`](../reference/usePrefetchQueries.md) or the [`usePrefetchInfiniteQuery`](../reference/usePrefetchInfiniteQuery.md) 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 Down
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,
},
],
})
})
})
184 changes: 184 additions & 0 deletions packages/react-query/src/__tests__/usePrefetchQueries.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
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 = <T,>(data: T) =>
vi.fn<(...args: Array<any>) => Promise<T>>().mockImplementation(async () => {
await sleep(10)

return data
})

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

function Suspended(props: {
queriesOpts: Array<UseSuspenseQueryOptions>
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(2),
}

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

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

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, 2'))
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(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, 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(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, 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(2),
}

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, 2'))
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
18 changes: 18 additions & 0 deletions packages/react-query/src/usePrefetchQueries.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useQueryClient } from './QueryClientProvider'

import type { FetchQueryOptions, QueryClient } from '@tanstack/query-core'

export function usePrefetchQueries(
options: {
queries: ReadonlyArray<FetchQueryOptions>
},
queryClient?: QueryClient,
) {
const client = useQueryClient(queryClient)

for (const query of options.queries) {
if (!client.getQueryState(query.queryKey)) {
client.prefetchQuery(query)
}
}
}
Loading