Skip to content

Commit d639a0d

Browse files
TkDodomanudeli
andauthored
feat(react-query): useSuspenseQuery (#5739)
* feat: useSuspenseQuery * feat: infiniteQueryOptions * fix: add exports * feat: useSuspenseInfiniteQuery * feat: initialData overloads for useInfiniteQuery * fix: types * chore: stabilize test we sometimes get failureCount: 2, but it doesn't matter here (timing issue) * fix: types for useSuspenseQuery (#5755) * docs: suspense * docs: api reference * docs: useSuspenseQuery in examples * fix: types for useSuspenseInfiniteQuery (#5766) --------- Co-authored-by: Jonghyeon Ko <[email protected]>
1 parent d82a0cb commit d639a0d

File tree

16 files changed

+364
-24
lines changed

16 files changed

+364
-24
lines changed

docs/react/guides/suspense.md

+44-3
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ id: suspense
33
title: Suspense
44
---
55

6-
> NOTE: Suspense mode for React Query is experimental, same as Suspense for data fetching itself. These APIs WILL change and should not be used in production unless you lock both your React and React Query versions to patch-level versions that are compatible with each other.
7-
8-
React Query can also be used with React's new Suspense for Data Fetching API's. To enable this mode, you can set either the global or query level config's `suspense` option to `true`.
6+
React Query can also be used with React's Suspense for Data Fetching API's. To enable this mode, you can set either the global or query level config's `suspense` option to `true`.
97

108
Global configuration:
119

@@ -98,10 +96,53 @@ const App: React.FC = () => {
9896
}
9997
```
10098

99+
## useSuspenseQuery
100+
101+
You can also use the dedicated `useSuspenseQuery` hook to enable suspense mode for a query:
102+
103+
```tsx
104+
import { useSuspenseQuery } from '@tanstack/react-query'
105+
106+
const { data } = useSuspenseQuery({ queryKey, queryFn })
107+
```
108+
109+
This has the same effect as setting the `suspense` option to `true` in the query config, but it works better in TypeScript, because `data` is guaranteed to be defined (as errors and loading states are handled by Suspense- and ErrorBoundaries).
110+
111+
On the flip side, you therefore can't conditionally enable / disable the Query. `placeholderData` also doesn't exist for this Query. To prevent the UI from being replaced by a fallback during an update, wrap your updates that change the QueryKey into [startTransition](https://react.dev/reference/react/Suspense#preventing-unwanted-fallbacks).
112+
101113
## Fetch-on-render vs Render-as-you-fetch
102114

103115
Out of the box, React Query in `suspense` mode works really well as a **Fetch-on-render** solution with no additional configuration. This means that when your components attempt to mount, they will trigger query fetching and suspend, but only once you have imported them and mounted them. If you want to take it to the next level and implement a **Render-as-you-fetch** model, we recommend implementing [Prefetching](../guides/prefetching) on routing callbacks and/or user interactions events to start loading queries before they are mounted and hopefully even before you start importing or mounting their parent components.
104116

117+
## Suspense on the Server with streaming
118+
119+
If you are using `NextJs`, you can use our **experimental** integration for Suspense on the Server: `@tanstack/react-query-next-experimental`. This package will allow you to fetch data on the server (in a client component) by just calling `useQuery` (with `suspense: true`) or `useSuspenseQuery` in your component. Results will then be streamed from the server to the client as SuspenseBoundaries resolve.
120+
121+
To achieve this, wrap your app in the `ReactQueryStreamedHydration` component:
122+
123+
```tsx
124+
// app/providers.tsx
125+
'use client'
126+
127+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
128+
import * as React from 'react'
129+
import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental'
130+
131+
export function Providers(props: { children: React.ReactNode }) {
132+
const [queryClient] = React.useState(() => new QueryClient())
133+
134+
return (
135+
<QueryClientProvider client={queryClient}>
136+
<ReactQueryStreamedHydration>
137+
{props.children}
138+
</ReactQueryStreamedHydration>
139+
</QueryClientProvider>
140+
)
141+
}
142+
```
143+
144+
For more information, check out the [NextJs Suspense Streaming Example](../examples/react/nextjs-suspense-streaming).
145+
105146
## Further reading
106147

107148
For tips on using suspense option, check the [Suspensive React Query Package](../community/suspensive-react-query) from the Community Resources.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
id: useSuspenseInfiniteQuery
3+
title: useSuspenseInfiniteQuery
4+
---
5+
6+
```tsx
7+
const result = useSuspenseInfiniteQuery(options)
8+
```
9+
10+
**Options**
11+
12+
The same as for [useInfiniteQuery](../reference/useInfiniteQuery), except for:
13+
- `suspense`
14+
- `throwOnError`
15+
- `enabled`
16+
- `placeholderData`
17+
18+
**Returns**
19+
20+
Same object as [useInfiniteQuery](../reference/useInfiniteQuery), except for:
21+
- `isPlaceholderData` is missing
22+
- `status` is always `success`
23+
- the derived flags are set accordingly.
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
id: useSuspenseQuery
3+
title: useSuspenseQuery
4+
---
5+
6+
```tsx
7+
const result = useSuspenseQuery(options)
8+
```
9+
10+
**Options**
11+
12+
The same as for [useQuery](../reference/useQuery), except for:
13+
- `suspense`
14+
- `throwOnError`
15+
- `enabled`
16+
- `placeholderData`
17+
18+
**Returns**
19+
20+
Same object as [useQuery](../reference/useQuery), except for:
21+
- `isPlaceholderData` is missing
22+
- `status` is always `success`
23+
- the derived flags are set accordingly.

examples/react/nextjs-suspense-streaming/src/app/page.tsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use client'
2-
import { useQuery } from '@tanstack/react-query'
2+
import { useSuspenseQuery } from '@tanstack/react-query'
33
import { Suspense } from 'react'
44

55
// export const runtime = "edge"; // 'nodejs' (default) | 'edge'
@@ -15,7 +15,7 @@ function getBaseURL() {
1515
}
1616
const baseUrl = getBaseURL()
1717
function useWaitQuery(props: { wait: number }) {
18-
const query = useQuery({
18+
const query = useSuspenseQuery({
1919
queryKey: ['wait', props.wait],
2020
queryFn: async () => {
2121
const path = `/api/wait?wait=${props.wait}`
@@ -29,7 +29,6 @@ function useWaitQuery(props: { wait: number }) {
2929
).json()
3030
return res
3131
},
32-
suspense: true,
3332
})
3433

3534
return [query.data as string, query] as const

examples/react/suspense/src/components/Project.jsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import React from 'react'
2-
import { useQuery } from '@tanstack/react-query'
2+
import { useSuspenseQuery } from '@tanstack/react-query'
33

44
import Button from './Button'
55
import Spinner from './Spinner'
66

77
import { fetchProject } from '../queries'
88

99
export default function Project({ activeProject, setActiveProject }) {
10-
const { data, isFetching } = useQuery({
10+
const { data, isFetching } = useSuspenseQuery({
1111
queryKey: ['project', activeProject],
1212
queryFn: () => fetchProject(activeProject),
1313
})

examples/react/suspense/src/components/Projects.jsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react'
2-
import { useQuery, useQueryClient } from '@tanstack/react-query'
2+
import { useSuspenseQuery, useQueryClient } from '@tanstack/react-query'
33

44
import Button from './Button'
55
import Spinner from './Spinner'
@@ -8,7 +8,7 @@ import { fetchProjects, fetchProject } from '../queries'
88

99
export default function Projects({ setActiveProject }) {
1010
const queryClient = useQueryClient()
11-
const { data, isFetching } = useQuery({
11+
const { data, isFetching } = useSuspenseQuery({
1212
queryKey: ['projects'],
1313
queryFn: fetchProjects,
1414
})

examples/react/suspense/src/index.jsx

-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ const queryClient = new QueryClient({
2020
defaultOptions: {
2121
queries: {
2222
retry: 0,
23-
suspense: true,
2423
},
2524
},
2625
})

packages/query-core/src/types.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -585,14 +585,20 @@ export interface InfiniteQueryObserverSuccessResult<
585585
status: 'success'
586586
}
587587

588+
export type DefinedInfiniteQueryObserverResult<
589+
TData = unknown,
590+
TError = DefaultError,
591+
> =
592+
| InfiniteQueryObserverRefetchErrorResult<TData, TError>
593+
| InfiniteQueryObserverSuccessResult<TData, TError>
594+
588595
export type InfiniteQueryObserverResult<
589596
TData = unknown,
590597
TError = DefaultError,
591598
> =
592599
| InfiniteQueryObserverLoadingErrorResult<TData, TError>
593600
| InfiniteQueryObserverLoadingResult<TData, TError>
594-
| InfiniteQueryObserverRefetchErrorResult<TData, TError>
595-
| InfiniteQueryObserverSuccessResult<TData, TError>
601+
| DefinedInfiniteQueryObserverResult<TData, TError>
596602

597603
export type MutationKey = readonly unknown[]
598604

packages/react-query/src/__tests__/useQuery.test.tsx

+1-4
Original file line numberDiff line numberDiff line change
@@ -5422,11 +5422,8 @@ describe('useQuery', () => {
54225422
const rendered = renderWithClient(queryClient, <Page />)
54235423

54245424
await waitFor(() =>
5425-
rendered.getByText(
5426-
'status: pending, fetchStatus: fetching, failureCount: 1',
5427-
),
5425+
rendered.getByText(/status: pending, fetchStatus: fetching/i),
54285426
)
5429-
await waitFor(() => rendered.getByText('failureReason: failed1'))
54305427

54315428
const onlineMock = mockOnlineManagerIsOnline(false)
54325429
window.dispatchEvent(new Event('offline'))

packages/react-query/src/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ export * from './types'
88
export { useQueries } from './useQueries'
99
export type { QueriesResults, QueriesOptions } from './useQueries'
1010
export { useQuery } from './useQuery'
11+
export { useSuspenseQuery } from './useSuspenseQuery'
12+
export { useSuspenseInfiniteQuery } from './useSuspenseInfiniteQuery'
1113
export { queryOptions } from './queryOptions'
14+
export { infiniteQueryOptions } from './infiniteQueryOptions'
1215
export {
1316
QueryClientContext,
1417
QueryClientProvider,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import type { InfiniteData } from '@tanstack/query-core'
2+
import type { UseInfiniteQueryOptions } from './types'
3+
import type { DefaultError, QueryKey } from '@tanstack/query-core'
4+
5+
export type UndefinedInitialDataInfiniteOptions<
6+
TQueryFnData = unknown,
7+
TError = DefaultError,
8+
TData = TQueryFnData,
9+
TQueryData = TQueryFnData,
10+
TQueryKey extends QueryKey = QueryKey,
11+
TPageParam = unknown,
12+
> = UseInfiniteQueryOptions<
13+
TQueryFnData,
14+
TError,
15+
TData,
16+
TQueryData,
17+
TQueryKey,
18+
TPageParam
19+
> & {
20+
initialData?: undefined
21+
}
22+
23+
export type DefinedInitialDataInfiniteOptions<
24+
TQueryFnData = unknown,
25+
TError = DefaultError,
26+
TData = TQueryFnData,
27+
TQueryData = TQueryFnData,
28+
TQueryKey extends QueryKey = QueryKey,
29+
TPageParam = unknown,
30+
> = UseInfiniteQueryOptions<
31+
TQueryFnData,
32+
TError,
33+
TData,
34+
TQueryData,
35+
TQueryKey,
36+
TPageParam
37+
> & {
38+
initialData: InfiniteData<TQueryData> | (() => InfiniteData<TQueryData>)
39+
}
40+
41+
export function infiniteQueryOptions<
42+
TQueryFnData = unknown,
43+
TError = DefaultError,
44+
TData = TQueryFnData,
45+
TQueryData = TQueryFnData,
46+
TQueryKey extends QueryKey = QueryKey,
47+
TPageParam = unknown,
48+
>(
49+
options: UndefinedInitialDataInfiniteOptions<
50+
TQueryFnData,
51+
TError,
52+
TData,
53+
TQueryFnData,
54+
TQueryKey,
55+
TPageParam
56+
>,
57+
): UndefinedInitialDataInfiniteOptions<
58+
TQueryFnData,
59+
TError,
60+
TData,
61+
TQueryFnData,
62+
TQueryKey,
63+
TPageParam
64+
>
65+
66+
export function infiniteQueryOptions<
67+
TQueryFnData = unknown,
68+
TError = DefaultError,
69+
TData = TQueryFnData,
70+
TQueryData = TQueryFnData,
71+
TQueryKey extends QueryKey = QueryKey,
72+
TPageParam = unknown,
73+
>(
74+
options: DefinedInitialDataInfiniteOptions<
75+
TQueryFnData,
76+
TError,
77+
TData,
78+
TQueryFnData,
79+
TQueryKey,
80+
TPageParam
81+
>,
82+
): DefinedInitialDataInfiniteOptions<
83+
TQueryFnData,
84+
TError,
85+
TData,
86+
TQueryFnData,
87+
TQueryKey,
88+
TPageParam
89+
>
90+
91+
export function infiniteQueryOptions(options: unknown) {
92+
return options
93+
}

0 commit comments

Comments
 (0)