Skip to content

Commit b20f1f7

Browse files
authored
feat(react-query): useSuspenseQueries (#5836)
* docs: suspense * feat: remove suspense boolean from useQuery and useInfiniteQuery * feat: useSuspenseQueries * docs: useSuspenseQueries * test: fix suspense in tests * docs: offline caching is not experimental * tests: there is no throwOnError on useSuspenseQueries * chore: exports * chore: try to stabilize a flaky test
1 parent b3301ac commit b20f1f7

14 files changed

+327
-500
lines changed

docs/react/comparison.md

Lines changed: 46 additions & 46 deletions
Large diffs are not rendered by default.

docs/react/guides/migrating-to-v5.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ The `loading` status has been renamed to `pending`, and similarly the derived `i
400400

401401
For mutations as well the `status` has been changed from `loading` to `pending` and the `isLoading` flag has been changed to `isPending`.
402402

403-
Lastly the a new derived `isLoading` flag has been added to the queries that is implemented as `isPending && isFetching`. This means that `isLoading` and `isInitialLoading` have the same thing, but `isInitialLoading` is deprecated now and will be removed in the next major version.
403+
Lastly, a new derived `isLoading` flag has been added to the queries that is implemented as `isPending && isFetching`. This means that `isLoading` and `isInitialLoading` have the same thing, but `isInitialLoading` is deprecated now and will be removed in the next major version.
404404

405405
To understand the reasoning behing this change checkout the [v5 roadmap discussion](https://github.com/TanStack/query/discussions/4252).
406406

@@ -462,4 +462,20 @@ See the [TypeScript docs](../typescript#typing-query-options) for more details.
462462

463463
See the [useQueries docs](../reference/useQueries#combine) for more details.
464464

465+
### new hooks for suspense
466+
467+
With v5, suspense for data fetching finally becomes "stable". We've added dedicated `useSuspenseQuery`, `useSuspenseInfiniteQuery` and `useSuspenseQueries` hooks. With these hooks, `data` will never be potentially `undefined` on type level:
468+
469+
```js
470+
const [post] = useSuspenseQuery({
471+
// ^? const post: Post
472+
queryKey: ['post', postId],
473+
queryFn: () => fetchPost(postId),
474+
})
475+
```
476+
477+
The experimental `suspense: boolean` flag on the query hooks has been removed.
478+
479+
You can read more about them in the [suspense docs](../guides/suspense).
480+
465481
[//]: # 'NewFeatures'

docs/react/guides/parallel-queries.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ function App () {
2424
[//]: # 'Example'
2525
[//]: # 'Info'
2626

27-
> When using React Query in suspense mode, this pattern of parallelism does not work, since the first query would throw a promise internally and would suspend the component before the other queries run. To get around this, you'll either need to use the `useQueries` hook (which is suggested) or orchestrate your own parallelism with separate components for each `useQuery` instance (which is lame).
27+
> When using React Query in suspense mode, this pattern of parallelism does not work, since the first query would throw a promise internally and would suspend the component before the other queries run. To get around this, you'll either need to use the `useSuspenseQueries` hook (which is suggested) or orchestrate your own parallelism with separate components for each `useSuspenseQuery` instance.
2828
2929
[//]: # 'Info'
3030

docs/react/guides/suspense.md

Lines changed: 11 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,27 @@ id: suspense
33
title: Suspense
44
---
55

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`.
6+
React Query can also be used with React's Suspense for Data Fetching API's. For this, we have dedicated hooks:
77

8-
Global configuration:
8+
- [useSuspenseQuery](../reference/useSuspenseQuery)
9+
- [useSuspenseInfiniteQuery](../reference/useSuspenseInfiniteQuery)
10+
- [useSuspenseQueries](../reference/useSuspenseQueries)
911

10-
```tsx
11-
// Configure for all queries
12-
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
13-
14-
const queryClient = new QueryClient({
15-
defaultOptions: {
16-
queries: {
17-
suspense: true,
18-
},
19-
},
20-
})
12+
When using suspense mode, `status` states and `error` objects are not needed and are then replaced by usage of the `React.Suspense` component (including the use of the `fallback` prop and React error boundaries for catching errors). Please read the [Resetting Error Boundaries](#resetting-error-boundaries) and look at the [Suspense Example](https://codesandbox.io/s/github/tannerlinsley/react-query/tree/main/examples/react/suspense) for more information on how to set up suspense mode.
2113

22-
function Root() {
23-
return (
24-
<QueryClientProvider client={queryClient}>
25-
<App />
26-
</QueryClientProvider>
27-
)
28-
}
29-
```
14+
In addition to queries behaving differently in suspense mode, mutations also behave a bit differently. By default, instead of supplying the `error` variable when a mutation fails, it will be thrown during the next render of the component it's used in and propagate to the nearest error boundary, similar to query errors. If you wish to disable this, you can set the `throwOnError` option to `false`. If you wish that errors are not thrown at all, you can set the `throwOnError` option to `false` as well!
3015

31-
Query configuration:
16+
Enabling suspense mode for a query:
3217

3318
```tsx
34-
import { useQuery } from '@tanstack/react-query'
19+
import { useSuspenseQuery } from '@tanstack/react-query'
3520

36-
// Enable for an individual query
37-
useQuery({ queryKey, queryFn, suspense: true })
21+
const { data } = useSuspenseQuery({ queryKey, queryFn })
3822
```
3923

40-
When using suspense mode, `status` states and `error` objects are not needed and are then replaced by usage of the `React.Suspense` component (including the use of the `fallback` prop and React error boundaries for catching errors). Please read the [Resetting Error Boundaries](#resetting-error-boundaries) and look at the [Suspense Example](https://codesandbox.io/s/github/tannerlinsley/react-query/tree/main/examples/react/suspense) for more information on how to set up suspense mode.
24+
This works nicely in TypeScript, because `data` is guaranteed to be defined (as errors and loading states are handled by Suspense- and ErrorBoundaries).
4125

42-
In addition to queries behaving differently in suspense mode, mutations also behave a bit differently. By default, instead of supplying the `error` variable when a mutation fails, it will be thrown during the next render of the component it's used in and propagate to the nearest error boundary, similar to query errors. If you wish to disable this, you can set the `throwOnError` option to `false`. If you wish that errors are not thrown at all, you can set the `throwOnError` option to `false` as well!
26+
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).
4327

4428
## Resetting Error Boundaries
4529

@@ -96,20 +80,6 @@ const App: React.FC = () => {
9680
}
9781
```
9882

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-
11383
## Fetch-on-render vs Render-as-you-fetch
11484

11585
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.

docs/react/reference/useSuspenseInfiniteQuery.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ The same as for [useInfiniteQuery](../reference/useInfiniteQuery), except for:
1717

1818
**Returns**
1919

20-
Same object as [useInfiniteQuery](../reference/useInfiniteQuery), except for:
20+
Same object as [useInfiniteQuery](../reference/useInfiniteQuery), except that:
21+
- `data` is guaranteed to be defined
2122
- `isPlaceholderData` is missing
2223
- `status` is always `success`
2324
- the derived flags are set accordingly.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
id: useSuspenseQueries
3+
title: useSuspenseQueries
4+
---
5+
6+
```tsx
7+
const result = useSuspenseQueries(options)
8+
```
9+
10+
**Options**
11+
12+
The same as for [useQueries](../reference/useQueries), except that each `query` can't have:
13+
- `suspense`
14+
- `throwOnError`
15+
- `enabled`
16+
- `placeholderData`
17+
18+
**Returns**
19+
20+
Same structure as [useQueries](../reference/useQueries), except that for each `query`:
21+
- `data` is guaranteed to be defined
22+
- `isPlaceholderData` is missing
23+
- `status` is always `success`
24+
- the derived flags are set accordingly.

docs/react/reference/useSuspenseQuery.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ The same as for [useQuery](../reference/useQuery), except for:
1717

1818
**Returns**
1919

20-
Same object as [useQuery](../reference/useQuery), except for:
20+
Same object as [useQuery](../reference/useQuery), except that:
21+
- `data` is guaranteed to be defined
2122
- `isPlaceholderData` is missing
2223
- `status` is always `success`
2324
- the derived flags are set accordingly.

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@ import { ErrorBoundary } from 'react-error-boundary'
33
import * as React from 'react'
44

55
import { vi } from 'vitest'
6-
import { QueryCache, QueryErrorResetBoundary, useQueries, useQuery } from '..'
6+
import {
7+
QueryCache,
8+
QueryErrorResetBoundary,
9+
useQueries,
10+
useQuery,
11+
useSuspenseQueries,
12+
useSuspenseQuery,
13+
} from '..'
714
import { createQueryClient, queryKey, renderWithClient, sleep } from './utils'
815

916
// TODO: This should be removed with the types for react-error-boundary get updated.
@@ -522,7 +529,7 @@ describe('QueryErrorResetBoundary', () => {
522529
let renders = 0
523530

524531
function Page() {
525-
const { data } = useQuery({
532+
const { data } = useSuspenseQuery({
526533
queryKey: key,
527534
queryFn: async () => {
528535
fetchCount++
@@ -534,7 +541,6 @@ describe('QueryErrorResetBoundary', () => {
534541
}
535542
},
536543
retry: false,
537-
suspense: true,
538544
})
539545
renders++
540546
return <div>{data}</div>
@@ -735,7 +741,7 @@ describe('QueryErrorResetBoundary', () => {
735741
let succeed = false
736742

737743
function Page() {
738-
const [{ data }] = useQueries({
744+
const [{ data }] = useSuspenseQueries({
739745
queries: [
740746
{
741747
queryKey: key,
@@ -748,9 +754,7 @@ describe('QueryErrorResetBoundary', () => {
748754
}
749755
},
750756
retry: false,
751-
throwOnError: true,
752757
retryOnMount: true,
753-
suspense: true,
754758
},
755759
],
756760
})

0 commit comments

Comments
 (0)