From 4175f2eb45434c412c2f452424ad4eadddd2cda9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Fri, 28 Feb 2025 15:41:32 +0000 Subject: [PATCH 01/11] feat(react-query): Add `usePrefetchQueries` hook --- .../__tests__/usePrefetchQueries.test-d.tsx | 56 +++++ .../src/__tests__/usePrefetchQueries.test.tsx | 196 ++++++++++++++++++ packages/react-query/src/index.ts | 1 + .../react-query/src/usePrefetchQueries.tsx | 17 ++ 4 files changed, 270 insertions(+) create mode 100644 packages/react-query/src/__tests__/usePrefetchQueries.test-d.tsx create mode 100644 packages/react-query/src/__tests__/usePrefetchQueries.test.tsx create mode 100644 packages/react-query/src/usePrefetchQueries.tsx diff --git a/packages/react-query/src/__tests__/usePrefetchQueries.test-d.tsx b/packages/react-query/src/__tests__/usePrefetchQueries.test-d.tsx new file mode 100644 index 0000000000..8ef597da99 --- /dev/null +++ b/packages/react-query/src/__tests__/usePrefetchQueries.test-d.tsx @@ -0,0 +1,56 @@ +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(5), + }, + ], + }) + + expectTypeOf(result).toEqualTypeOf() + }) + + 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(5), + // @ts-expect-error TS2345 + enabled: true, + }, + ], + }) + + usePrefetchQueries({ + queries: [ + { + queryKey: ['key1'], + queryFn: () => Promise.resolve(5), + // @ts-expect-error TS2345 + throwOnError: true, + }, + ], + }) + }) +}) diff --git a/packages/react-query/src/__tests__/usePrefetchQueries.test.tsx b/packages/react-query/src/__tests__/usePrefetchQueries.test.tsx new file mode 100644 index 0000000000..07534bc7c5 --- /dev/null +++ b/packages/react-query/src/__tests__/usePrefetchQueries.test.tsx @@ -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) => Promise>() + .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 ( +
+
data: {state.map((data) => String(data)).join(', ')}
+ {props.children} +
+ ) + } + + 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 ( + + + + ) + } + + const rendered = renderWithClient(queryClient, ) + + 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 ( + + + + ) + } + + await queryClient.fetchQuery(queryOpts1) + await queryClient.fetchQuery(queryOpts2) + queryOpts1.queryFn.mockClear() + queryOpts2.queryFn.mockClear() + + const rendered = renderWithClient(queryClient, ) + + 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 ( + + + + ) + } + + await queryClient.fetchQuery(queryOpts1) + queryOpts1.queryFn.mockClear() + queryOpts2.queryFn.mockClear() + + const rendered = renderWithClient(queryClient, ) + + 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 ( + + + + + + ) + } + + const rendered = renderWithClient(queryClient, ) + await waitFor(() => + rendered.getByText('data: prefetchedQuery1, prefetchedQuery2'), + ) + expect(queryOpts1.queryFn).toHaveBeenCalledTimes(1) + expect(queryOpts2.queryFn).toHaveBeenCalledTimes(1) + }) +}) diff --git a/packages/react-query/src/index.ts b/packages/react-query/src/index.ts index 5f372f4195..4adfab7807 100644 --- a/packages/react-query/src/index.ts +++ b/packages/react-query/src/index.ts @@ -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 { diff --git a/packages/react-query/src/usePrefetchQueries.tsx b/packages/react-query/src/usePrefetchQueries.tsx new file mode 100644 index 0000000000..a449c06cb2 --- /dev/null +++ b/packages/react-query/src/usePrefetchQueries.tsx @@ -0,0 +1,17 @@ +import { useQueryClient } from './QueryClientProvider' +import type { FetchQueryOptions, QueryClient } from '@tanstack/query-core' + +export function usePrefetchQueries( + options: { + queries: ReadonlyArray> + }, + queryClient?: QueryClient, +) { + const client = useQueryClient(queryClient) + + for (const query of options.queries) { + if (!client.getQueryState(query.queryKey)) { + client.prefetchQuery(query) + } + } +} From 710c06bd7e19edbf7a23d605b16cd9a97e9aa297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Wed, 5 Mar 2025 15:28:56 +0000 Subject: [PATCH 02/11] docs(react-query): reference for usePrefetchQueries --- docs/config.json | 6 ++- .../react/reference/usePrefetchQueries.md | 40 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 docs/framework/react/reference/usePrefetchQueries.md diff --git a/docs/config.json b/docs/config.json index 8316f688ed..3c0152ec86 100644 --- a/docs/config.json +++ b/docs/config.json @@ -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" @@ -1162,4 +1166,4 @@ "Nozzle.io", "Uber" ] -} +} \ No newline at end of file diff --git a/docs/framework/react/reference/usePrefetchQueries.md b/docs/framework/react/reference/usePrefetchQueries.md new file mode 100644 index 0000000000..8516239a5d --- /dev/null +++ b/docs/framework/react/reference/usePrefetchQueries.md @@ -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` + - **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). From 361f704e5a848f6363f1711417d0d98ad3660791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Wed, 5 Mar 2025 15:31:00 +0000 Subject: [PATCH 03/11] docs(react-query): usePrefetchQueries docs --- docs/framework/react/guides/prefetching.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/framework/react/guides/prefetching.md b/docs/framework/react/guides/prefetching.md index ccdb5ce9df..c56afbb38b 100644 --- a/docs/framework/react/guides/prefetching.md +++ b/docs/framework/react/guides/prefetching.md @@ -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), the [`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 `` boundary so the "secondary" query we are prefetching does not block rendering of the "primary" data. @@ -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 From 32caa07da9100f507fa7c854474743e03ad3ed68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Wed, 5 Mar 2025 15:41:29 +0000 Subject: [PATCH 04/11] docs(react-query): broken prefetching links --- docs/framework/react/guides/prefetching.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/framework/react/guides/prefetching.md b/docs/framework/react/guides/prefetching.md index c56afbb38b..babcaa0845 100644 --- a/docs/framework/react/guides/prefetching.md +++ b/docs/framework/react/guides/prefetching.md @@ -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 @@ -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), the [`usePrefetchQueries`](../../reference/usePrefetchQueries) 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), the [`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 `` boundary so the "secondary" query we are prefetching does not block rendering of the "primary" data. @@ -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' @@ -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' @@ -431,8 +431,8 @@ queryClient.setQueryData(['todos'], todos) ## Further reading -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. +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](https://tkdodo.eu/blog/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' From 75ba89ff18c82c73c6ad6f11c7b1d05bed18f11c Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 5 Mar 2025 15:42:38 +0000 Subject: [PATCH 05/11] ci: apply automated fixes --- docs/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.json b/docs/config.json index 3c0152ec86..06892d8d6e 100644 --- a/docs/config.json +++ b/docs/config.json @@ -1166,4 +1166,4 @@ "Nozzle.io", "Uber" ] -} \ No newline at end of file +} From afdf7ec9d2c76b263d85179e167069fd7eebd960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Wed, 12 Mar 2025 14:13:48 +0000 Subject: [PATCH 06/11] fix(react-query): enforce array of options types --- .../react-query/src/usePrefetchQueries.tsx | 98 ++++++++++++++++++- 1 file changed, 95 insertions(+), 3 deletions(-) diff --git a/packages/react-query/src/usePrefetchQueries.tsx b/packages/react-query/src/usePrefetchQueries.tsx index a449c06cb2..7bfd79a167 100644 --- a/packages/react-query/src/usePrefetchQueries.tsx +++ b/packages/react-query/src/usePrefetchQueries.tsx @@ -1,9 +1,101 @@ import { useQueryClient } from './QueryClientProvider' -import type { FetchQueryOptions, QueryClient } from '@tanstack/query-core' -export function usePrefetchQueries( +import type { + FetchQueryOptions, + QueryClient, + QueryFunction, + ThrowOnError, +} from '@tanstack/query-core' + +// Avoid TS depth-limit error in case of large array literal +type MAXIMUM_DEPTH = 20 + +// Widen the type of the symbol to enable type inference even if skipToken is not immutable. +type SkipTokenForFetchQuery = symbol + +type GetFetchQueryOptions = + // Part 1: responsible for applying explicit type parameter to function arguments, if object { queryFnData: TQueryFnData, error: TError, data: TData } + T extends { + queryFnData: infer TQueryFnData + error?: infer TError + data: infer TData + } + ? FetchQueryOptions + : T extends { queryFnData: infer TQueryFnData; error?: infer TError } + ? FetchQueryOptions + : T extends { data: infer TData; error?: infer TError } + ? FetchQueryOptions + : // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData] + T extends [infer TQueryFnData, infer TError, infer TData] + ? FetchQueryOptions + : T extends [infer TQueryFnData, infer TError] + ? FetchQueryOptions + : T extends [infer TQueryFnData] + ? FetchQueryOptions + : // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided + T extends { + queryFn?: + | QueryFunction + | SkipTokenForFetchQuery + select?: (data: any) => infer TData + throwOnError?: ThrowOnError + } + ? FetchQueryOptions + : T extends { + queryFn?: + | QueryFunction + | SkipTokenForFetchQuery + throwOnError?: ThrowOnError + } + ? FetchQueryOptions< + TQueryFnData, + TError, + TQueryFnData, + TQueryKey + > + : // Fallback + FetchQueryOptions + +/** + * SuspenseQueriesOptions reducer recursively unwraps function arguments to infer/enforce type param + */ +export type PrefetchQueriesOptions< + T extends Array, + TResults extends Array = [], + TDepth extends ReadonlyArray = [], +> = TDepth['length'] extends MAXIMUM_DEPTH + ? Array + : T extends [] + ? [] + : T extends [infer Head] + ? [...TResults, GetFetchQueryOptions] + : T extends [infer Head, ...infer Tails] + ? PrefetchQueriesOptions< + [...Tails], + [...TResults, GetFetchQueryOptions], + [...TDepth, 1] + > + : Array extends T + ? T + : // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogenous type! + // use this to infer the param types in the case of Array.map() argument + T extends Array< + FetchQueryOptions< + infer TQueryFnData, + infer TError, + infer TData, + infer TQueryKey + > + > + ? Array> + : // Fallback + Array + +export function usePrefetchQueries>( options: { - queries: ReadonlyArray> + queries: + | readonly [...PrefetchQueriesOptions] + | readonly [...{ [K in keyof T]: GetFetchQueryOptions }] }, queryClient?: QueryClient, ) { From 8542f89df3cb842f0904f7ee4c213a0b588dac42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Wed, 12 Mar 2025 15:41:44 +0000 Subject: [PATCH 07/11] fix(react-query): update types --- .../__tests__/usePrefetchQueries.test-d.tsx | 18 +++++++++++++++--- .../react-query/src/usePrefetchQueries.tsx | 5 +++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/react-query/src/__tests__/usePrefetchQueries.test-d.tsx b/packages/react-query/src/__tests__/usePrefetchQueries.test-d.tsx index 8ef597da99..05c263207d 100644 --- a/packages/react-query/src/__tests__/usePrefetchQueries.test-d.tsx +++ b/packages/react-query/src/__tests__/usePrefetchQueries.test-d.tsx @@ -11,7 +11,15 @@ describe('usePrefetchQueries', () => { }, { queryKey: ['key2'], - queryFn: () => Promise.resolve(5), + queryFn: () => Promise.resolve('data'), + }, + { + queryKey: ['key3'], + queryFn: () => + Promise.resolve({ + foo: 1, + bar: 'fizzbuzz', + }), }, ], }) @@ -35,7 +43,7 @@ describe('usePrefetchQueries', () => { queries: [ { queryKey: ['key1'], - queryFn: () => Promise.resolve(5), + queryFn: () => Promise.resolve('data'), // @ts-expect-error TS2345 enabled: true, }, @@ -46,7 +54,11 @@ describe('usePrefetchQueries', () => { queries: [ { queryKey: ['key1'], - queryFn: () => Promise.resolve(5), + queryFn: () => + Promise.resolve({ + foo: 1, + bar: 'fizzbuzz', + }), // @ts-expect-error TS2345 throwOnError: true, }, diff --git a/packages/react-query/src/usePrefetchQueries.tsx b/packages/react-query/src/usePrefetchQueries.tsx index 7bfd79a167..d1b7320d97 100644 --- a/packages/react-query/src/usePrefetchQueries.tsx +++ b/packages/react-query/src/usePrefetchQueries.tsx @@ -57,7 +57,7 @@ type GetFetchQueryOptions = FetchQueryOptions /** - * SuspenseQueriesOptions reducer recursively unwraps function arguments to infer/enforce type param + * PrefetchQueriesOptions reducer recursively unwraps function arguments to infer/enforce type param */ export type PrefetchQueriesOptions< T extends Array, @@ -100,8 +100,9 @@ export function usePrefetchQueries>( queryClient?: QueryClient, ) { const client = useQueryClient(queryClient) + const queries = options.queries as ReadonlyArray - for (const query of options.queries) { + for (const query of queries) { if (!client.getQueryState(query.queryKey)) { client.prefetchQuery(query) } From 336539767df9c798482e3a586e7604b54f58c178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Wed, 12 Mar 2025 16:00:07 +0000 Subject: [PATCH 08/11] docs(react-query): small fix --- docs/framework/react/guides/prefetching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/framework/react/guides/prefetching.md b/docs/framework/react/guides/prefetching.md index babcaa0845..1df9924ec7 100644 --- a/docs/framework/react/guides/prefetching.md +++ b/docs/framework/react/guides/prefetching.md @@ -431,7 +431,7 @@ queryClient.setQueryData(['todos'], todos) ## Further reading -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](https://tkdodo.eu/blog/seeding-the-query-cache) from the Community Resources. +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](../guides/ssr). From ad2850699ed88ea1064ea4c68550e34075a94a48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Wed, 12 Mar 2025 16:03:56 +0000 Subject: [PATCH 09/11] docs(react-query): grammer --- docs/framework/react/guides/prefetching.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/framework/react/guides/prefetching.md b/docs/framework/react/guides/prefetching.md index 1df9924ec7..0e389f879d 100644 --- a/docs/framework/react/guides/prefetching.md +++ b/docs/framework/react/guides/prefetching.md @@ -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), the [`usePrefetchQueries`](../reference/usePrefetchQueries) 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 `` boundary so the "secondary" query we are prefetching does not block rendering of the "primary" data. @@ -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). From cba33ee17c34333b7282532988dd6f946a561cc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Sat, 5 Apr 2025 21:47:01 +0100 Subject: [PATCH 10/11] chore: remove unused complicated types --- .../react-query/src/usePrefetchQueries.tsx | 100 +----------------- 1 file changed, 4 insertions(+), 96 deletions(-) diff --git a/packages/react-query/src/usePrefetchQueries.tsx b/packages/react-query/src/usePrefetchQueries.tsx index d1b7320d97..c967752e1e 100644 --- a/packages/react-query/src/usePrefetchQueries.tsx +++ b/packages/react-query/src/usePrefetchQueries.tsx @@ -1,108 +1,16 @@ import { useQueryClient } from './QueryClientProvider' -import type { - FetchQueryOptions, - QueryClient, - QueryFunction, - ThrowOnError, -} from '@tanstack/query-core' +import type { FetchQueryOptions, QueryClient } from '@tanstack/query-core' -// Avoid TS depth-limit error in case of large array literal -type MAXIMUM_DEPTH = 20 - -// Widen the type of the symbol to enable type inference even if skipToken is not immutable. -type SkipTokenForFetchQuery = symbol - -type GetFetchQueryOptions = - // Part 1: responsible for applying explicit type parameter to function arguments, if object { queryFnData: TQueryFnData, error: TError, data: TData } - T extends { - queryFnData: infer TQueryFnData - error?: infer TError - data: infer TData - } - ? FetchQueryOptions - : T extends { queryFnData: infer TQueryFnData; error?: infer TError } - ? FetchQueryOptions - : T extends { data: infer TData; error?: infer TError } - ? FetchQueryOptions - : // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData] - T extends [infer TQueryFnData, infer TError, infer TData] - ? FetchQueryOptions - : T extends [infer TQueryFnData, infer TError] - ? FetchQueryOptions - : T extends [infer TQueryFnData] - ? FetchQueryOptions - : // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided - T extends { - queryFn?: - | QueryFunction - | SkipTokenForFetchQuery - select?: (data: any) => infer TData - throwOnError?: ThrowOnError - } - ? FetchQueryOptions - : T extends { - queryFn?: - | QueryFunction - | SkipTokenForFetchQuery - throwOnError?: ThrowOnError - } - ? FetchQueryOptions< - TQueryFnData, - TError, - TQueryFnData, - TQueryKey - > - : // Fallback - FetchQueryOptions - -/** - * PrefetchQueriesOptions reducer recursively unwraps function arguments to infer/enforce type param - */ -export type PrefetchQueriesOptions< - T extends Array, - TResults extends Array = [], - TDepth extends ReadonlyArray = [], -> = TDepth['length'] extends MAXIMUM_DEPTH - ? Array - : T extends [] - ? [] - : T extends [infer Head] - ? [...TResults, GetFetchQueryOptions] - : T extends [infer Head, ...infer Tails] - ? PrefetchQueriesOptions< - [...Tails], - [...TResults, GetFetchQueryOptions], - [...TDepth, 1] - > - : Array extends T - ? T - : // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogenous type! - // use this to infer the param types in the case of Array.map() argument - T extends Array< - FetchQueryOptions< - infer TQueryFnData, - infer TError, - infer TData, - infer TQueryKey - > - > - ? Array> - : // Fallback - Array - -export function usePrefetchQueries>( +export function usePrefetchQueries( options: { - queries: - | readonly [...PrefetchQueriesOptions] - | readonly [...{ [K in keyof T]: GetFetchQueryOptions }] + queries: ReadonlyArray }, queryClient?: QueryClient, ) { const client = useQueryClient(queryClient) - const queries = options.queries as ReadonlyArray - for (const query of queries) { + for (const query of options.queries) { if (!client.getQueryState(query.queryKey)) { client.prefetchQuery(query) } From 5f81470810b1b8d935d9cacc76cdf9190202a0fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Curley?= Date: Tue, 8 Apr 2025 12:34:03 +0100 Subject: [PATCH 11/11] tests(react-query): allow multiple types in query returns --- .../src/__tests__/usePrefetchQueries.test.tsx | 44 +++++++------------ 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/packages/react-query/src/__tests__/usePrefetchQueries.test.tsx b/packages/react-query/src/__tests__/usePrefetchQueries.test.tsx index 07534bc7c5..4a306414e4 100644 --- a/packages/react-query/src/__tests__/usePrefetchQueries.test.tsx +++ b/packages/react-query/src/__tests__/usePrefetchQueries.test.tsx @@ -6,23 +6,19 @@ import { createQueryClient, queryKey, renderWithClient, sleep } from './utils' import type { UseSuspenseQueryOptions } from '..' -const generateQueryFn = (data: string) => - vi - .fn<(...args: Array) => Promise>() - .mockImplementation(async () => { - await sleep(10) +const generateQueryFn = (data: T) => + vi.fn<(...args: Array) => Promise>().mockImplementation(async () => { + await sleep(10) - return data - }) + return data + }) describe('usePrefetchQueries', () => { const queryCache = new QueryCache() const queryClient = createQueryClient({ queryCache }) - function Suspended(props: { - queriesOpts: Array< - UseSuspenseQueryOptions> - > + function Suspended(props: { + queriesOpts: Array children?: React.ReactNode }) { const state = useSuspenseQueries({ @@ -46,7 +42,7 @@ describe('usePrefetchQueries', () => { const queryOpts2 = { queryKey: queryKey(), - queryFn: generateQueryFn('prefetchQuery2'), + queryFn: generateQueryFn(2), } const componentQueryOpts1 = { @@ -56,7 +52,7 @@ describe('usePrefetchQueries', () => { const componentQueryOpts2 = { ...queryOpts2, - queryFn: generateQueryFn('useSuspenseQuery2'), + queryFn: generateQueryFn(2), } function App() { @@ -73,9 +69,7 @@ describe('usePrefetchQueries', () => { const rendered = renderWithClient(queryClient, ) - await waitFor(() => - rendered.getByText('data: prefetchQuery1, prefetchQuery2'), - ) + await waitFor(() => rendered.getByText('data: prefetchQuery1, 2')) expect(queryOpts1.queryFn).toHaveBeenCalledTimes(1) expect(queryOpts2.queryFn).toHaveBeenCalledTimes(1) }) @@ -88,7 +82,7 @@ describe('usePrefetchQueries', () => { const queryOpts2 = { queryKey: queryKey(), - queryFn: generateQueryFn('The usePrefetchQueries hook is smart! 2'), + queryFn: generateQueryFn(2), } function App() { @@ -112,9 +106,7 @@ describe('usePrefetchQueries', () => { expect(rendered.queryByText('fetching: true')).not.toBeInTheDocument() await waitFor(() => - rendered.getByText( - 'data: The usePrefetchQueries hook is smart! 1, The usePrefetchQueries hook is smart! 2', - ), + rendered.getByText('data: The usePrefetchQueries hook is smart! 1, 2'), ) expect(queryOpts1.queryFn).not.toHaveBeenCalled() expect(queryOpts2.queryFn).not.toHaveBeenCalled() @@ -128,7 +120,7 @@ describe('usePrefetchQueries', () => { const queryOpts2 = { queryKey: queryKey(), - queryFn: generateQueryFn('The usePrefetchQueries hook is smart! 2'), + queryFn: generateQueryFn(2), } function App() { @@ -150,9 +142,7 @@ describe('usePrefetchQueries', () => { const rendered = renderWithClient(queryClient, ) await waitFor(() => - rendered.getByText( - 'data: The usePrefetchQueries hook is smart! 1, The usePrefetchQueries hook is smart! 2', - ), + rendered.getByText('data: The usePrefetchQueries hook is smart! 1, 2'), ) expect(queryOpts1.queryFn).not.toHaveBeenCalled() expect(queryOpts2.queryFn).toHaveBeenCalledTimes(1) @@ -166,7 +156,7 @@ describe('usePrefetchQueries', () => { const queryOpts2 = { queryKey: queryKey(), - queryFn: generateQueryFn('prefetchedQuery2'), + queryFn: generateQueryFn(2), } function Prefetch({ children }: { children: React.ReactNode }) { @@ -187,9 +177,7 @@ describe('usePrefetchQueries', () => { } const rendered = renderWithClient(queryClient, ) - await waitFor(() => - rendered.getByText('data: prefetchedQuery1, prefetchedQuery2'), - ) + await waitFor(() => rendered.getByText('data: prefetchedQuery1, 2')) expect(queryOpts1.queryFn).toHaveBeenCalledTimes(1) expect(queryOpts2.queryFn).toHaveBeenCalledTimes(1) })