Skip to content

Commit b3564b1

Browse files
committed
[feat(react-query)] Add usePrefetchQueries hook
1 parent 18e357c commit b3564b1

File tree

4 files changed

+270
-0
lines changed

4 files changed

+270
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { describe, expectTypeOf, it } from 'vitest'
2+
import { usePrefetchQueries } from '..'
3+
4+
describe('usePrefetchQueries', () => {
5+
it('should return nothing', () => {
6+
const result = usePrefetchQueries({
7+
queries: [
8+
{
9+
queryKey: ['key1'],
10+
queryFn: () => Promise.resolve(5),
11+
},
12+
{
13+
queryKey: ['key2'],
14+
queryFn: () => Promise.resolve(5),
15+
},
16+
],
17+
})
18+
19+
expectTypeOf(result).toEqualTypeOf<void>()
20+
})
21+
22+
it('should not allow refetchInterval, enabled or throwOnError options', () => {
23+
usePrefetchQueries({
24+
queries: [
25+
{
26+
queryKey: ['key1'],
27+
queryFn: () => Promise.resolve(5),
28+
// @ts-expect-error TS2345
29+
refetchInterval: 1000,
30+
},
31+
],
32+
})
33+
34+
usePrefetchQueries({
35+
queries: [
36+
{
37+
queryKey: ['key1'],
38+
queryFn: () => Promise.resolve(5),
39+
// @ts-expect-error TS2345
40+
enabled: true,
41+
},
42+
],
43+
})
44+
45+
usePrefetchQueries({
46+
queries: [
47+
{
48+
queryKey: ['key1'],
49+
queryFn: () => Promise.resolve(5),
50+
// @ts-expect-error TS2345
51+
throwOnError: true,
52+
},
53+
],
54+
})
55+
})
56+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
import React from 'react'
3+
import { waitFor } from '@testing-library/react'
4+
import { QueryCache, usePrefetchQueries, useSuspenseQueries } from '..'
5+
import { createQueryClient, queryKey, renderWithClient, sleep } from './utils'
6+
7+
import type { UseSuspenseQueryOptions } from '..'
8+
9+
const generateQueryFn = (data: string) =>
10+
vi
11+
.fn<(...args: Array<any>) => Promise<string>>()
12+
.mockImplementation(async () => {
13+
await sleep(10)
14+
15+
return data
16+
})
17+
18+
describe('usePrefetchQueries', () => {
19+
const queryCache = new QueryCache()
20+
const queryClient = createQueryClient({ queryCache })
21+
22+
function Suspended<TData = unknown>(props: {
23+
queriesOpts: Array<
24+
UseSuspenseQueryOptions<TData, Error, TData, Array<string>>
25+
>
26+
children?: React.ReactNode
27+
}) {
28+
const state = useSuspenseQueries({
29+
queries: props.queriesOpts,
30+
combine: (results) => results.map((r) => r.data),
31+
})
32+
33+
return (
34+
<div>
35+
<div>data: {state.map((data) => String(data)).join(', ')}</div>
36+
{props.children}
37+
</div>
38+
)
39+
}
40+
41+
it('should prefetch queries if query states do not exist', async () => {
42+
const queryOpts1 = {
43+
queryKey: queryKey(),
44+
queryFn: generateQueryFn('prefetchQuery1'),
45+
}
46+
47+
const queryOpts2 = {
48+
queryKey: queryKey(),
49+
queryFn: generateQueryFn('prefetchQuery2'),
50+
}
51+
52+
const componentQueryOpts1 = {
53+
...queryOpts1,
54+
queryFn: generateQueryFn('useSuspenseQuery1'),
55+
}
56+
57+
const componentQueryOpts2 = {
58+
...queryOpts2,
59+
queryFn: generateQueryFn('useSuspenseQuery2'),
60+
}
61+
62+
function App() {
63+
usePrefetchQueries({
64+
queries: [queryOpts1, queryOpts2],
65+
})
66+
67+
return (
68+
<React.Suspense fallback="Loading...">
69+
<Suspended queriesOpts={[componentQueryOpts1, componentQueryOpts2]} />
70+
</React.Suspense>
71+
)
72+
}
73+
74+
const rendered = renderWithClient(queryClient, <App />)
75+
76+
await waitFor(() =>
77+
rendered.getByText('data: prefetchQuery1, prefetchQuery2'),
78+
)
79+
expect(queryOpts1.queryFn).toHaveBeenCalledTimes(1)
80+
expect(queryOpts2.queryFn).toHaveBeenCalledTimes(1)
81+
})
82+
83+
it('should not prefetch queries if query states exist', async () => {
84+
const queryOpts1 = {
85+
queryKey: queryKey(),
86+
queryFn: generateQueryFn('The usePrefetchQueries hook is smart! 1'),
87+
}
88+
89+
const queryOpts2 = {
90+
queryKey: queryKey(),
91+
queryFn: generateQueryFn('The usePrefetchQueries hook is smart! 2'),
92+
}
93+
94+
function App() {
95+
usePrefetchQueries({
96+
queries: [queryOpts1, queryOpts2],
97+
})
98+
99+
return (
100+
<React.Suspense fallback="Loading...">
101+
<Suspended queriesOpts={[queryOpts1, queryOpts2]} />
102+
</React.Suspense>
103+
)
104+
}
105+
106+
await queryClient.fetchQuery(queryOpts1)
107+
await queryClient.fetchQuery(queryOpts2)
108+
queryOpts1.queryFn.mockClear()
109+
queryOpts2.queryFn.mockClear()
110+
111+
const rendered = renderWithClient(queryClient, <App />)
112+
113+
expect(rendered.queryByText('fetching: true')).not.toBeInTheDocument()
114+
await waitFor(() =>
115+
rendered.getByText(
116+
'data: The usePrefetchQueries hook is smart! 1, The usePrefetchQueries hook is smart! 2',
117+
),
118+
)
119+
expect(queryOpts1.queryFn).not.toHaveBeenCalled()
120+
expect(queryOpts2.queryFn).not.toHaveBeenCalled()
121+
})
122+
123+
it('should only prefetch queries that do not exist', async () => {
124+
const queryOpts1 = {
125+
queryKey: queryKey(),
126+
queryFn: generateQueryFn('The usePrefetchQueries hook is smart! 1'),
127+
}
128+
129+
const queryOpts2 = {
130+
queryKey: queryKey(),
131+
queryFn: generateQueryFn('The usePrefetchQueries hook is smart! 2'),
132+
}
133+
134+
function App() {
135+
usePrefetchQueries({
136+
queries: [queryOpts1, queryOpts2],
137+
})
138+
139+
return (
140+
<React.Suspense fallback="Loading...">
141+
<Suspended queriesOpts={[queryOpts1, queryOpts2]} />
142+
</React.Suspense>
143+
)
144+
}
145+
146+
await queryClient.fetchQuery(queryOpts1)
147+
queryOpts1.queryFn.mockClear()
148+
queryOpts2.queryFn.mockClear()
149+
150+
const rendered = renderWithClient(queryClient, <App />)
151+
152+
await waitFor(() =>
153+
rendered.getByText(
154+
'data: The usePrefetchQueries hook is smart! 1, The usePrefetchQueries hook is smart! 2',
155+
),
156+
)
157+
expect(queryOpts1.queryFn).not.toHaveBeenCalled()
158+
expect(queryOpts2.queryFn).toHaveBeenCalledTimes(1)
159+
})
160+
161+
it('should not create an endless loop when using inside a suspense boundary', async () => {
162+
const queryOpts1 = {
163+
queryKey: queryKey(),
164+
queryFn: generateQueryFn('prefetchedQuery1'),
165+
}
166+
167+
const queryOpts2 = {
168+
queryKey: queryKey(),
169+
queryFn: generateQueryFn('prefetchedQuery2'),
170+
}
171+
172+
function Prefetch({ children }: { children: React.ReactNode }) {
173+
usePrefetchQueries({
174+
queries: [queryOpts1, queryOpts2],
175+
})
176+
return <>{children}</>
177+
}
178+
179+
function App() {
180+
return (
181+
<React.Suspense>
182+
<Prefetch>
183+
<Suspended queriesOpts={[queryOpts1, queryOpts2]} />
184+
</Prefetch>
185+
</React.Suspense>
186+
)
187+
}
188+
189+
const rendered = renderWithClient(queryClient, <App />)
190+
await waitFor(() =>
191+
rendered.getByText('data: prefetchedQuery1, prefetchedQuery2'),
192+
)
193+
expect(queryOpts1.queryFn).toHaveBeenCalledTimes(1)
194+
expect(queryOpts2.queryFn).toHaveBeenCalledTimes(1)
195+
})
196+
})

packages/react-query/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type {
1616
SuspenseQueriesOptions,
1717
} from './useSuspenseQueries'
1818
export { usePrefetchQuery } from './usePrefetchQuery'
19+
export { usePrefetchQueries } from './usePrefetchQueries'
1920
export { usePrefetchInfiniteQuery } from './usePrefetchInfiniteQuery'
2021
export { queryOptions } from './queryOptions'
2122
export type {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { useQueryClient } from './QueryClientProvider'
2+
import type { FetchQueryOptions, QueryClient } from '@tanstack/query-core'
3+
4+
export function usePrefetchQueries(
5+
options: {
6+
queries: ReadonlyArray<FetchQueryOptions<any, any, any, any>>
7+
},
8+
queryClient?: QueryClient,
9+
) {
10+
const client = useQueryClient(queryClient)
11+
12+
for (const query of options.queries) {
13+
if (!client.getQueryState(query.queryKey)) {
14+
client.prefetchQuery(query)
15+
}
16+
}
17+
}

0 commit comments

Comments
 (0)