Skip to content

Commit

Permalink
feat(react-query): useSuspenseQuery (#5739)
Browse files Browse the repository at this point in the history
* 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 <manudeli.ko@gmail.com>
  • Loading branch information
TkDodo and manudeli authored Jul 23, 2023
1 parent d82a0cb commit d639a0d
Show file tree
Hide file tree
Showing 16 changed files with 364 additions and 24 deletions.
47 changes: 44 additions & 3 deletions docs/react/guides/suspense.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ id: suspense
title: Suspense
---

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

Global configuration:

Expand Down Expand Up @@ -98,10 +96,53 @@ const App: React.FC = () => {
}
```

## useSuspenseQuery

You can also use the dedicated `useSuspenseQuery` hook to enable suspense mode for a query:

```tsx
import { useSuspenseQuery } from '@tanstack/react-query'

const { data } = useSuspenseQuery({ queryKey, queryFn })
```

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

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

## Fetch-on-render vs Render-as-you-fetch

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.

## Suspense on the Server with streaming

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.

To achieve this, wrap your app in the `ReactQueryStreamedHydration` component:

```tsx
// app/providers.tsx
'use client'

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import * as React from 'react'
import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental'

export function Providers(props: { children: React.ReactNode }) {
const [queryClient] = React.useState(() => new QueryClient())

return (
<QueryClientProvider client={queryClient}>
<ReactQueryStreamedHydration>
{props.children}
</ReactQueryStreamedHydration>
</QueryClientProvider>
)
}
```

For more information, check out the [NextJs Suspense Streaming Example](../examples/react/nextjs-suspense-streaming).

## Further reading

For tips on using suspense option, check the [Suspensive React Query Package](../community/suspensive-react-query) from the Community Resources.
23 changes: 23 additions & 0 deletions docs/react/reference/useSuspenseInfiniteQuery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
id: useSuspenseInfiniteQuery
title: useSuspenseInfiniteQuery
---

```tsx
const result = useSuspenseInfiniteQuery(options)
```

**Options**

The same as for [useInfiniteQuery](../reference/useInfiniteQuery), except for:
- `suspense`
- `throwOnError`
- `enabled`
- `placeholderData`

**Returns**

Same object as [useInfiniteQuery](../reference/useInfiniteQuery), except for:
- `isPlaceholderData` is missing
- `status` is always `success`
- the derived flags are set accordingly.
23 changes: 23 additions & 0 deletions docs/react/reference/useSuspenseQuery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
id: useSuspenseQuery
title: useSuspenseQuery
---

```tsx
const result = useSuspenseQuery(options)
```

**Options**

The same as for [useQuery](../reference/useQuery), except for:
- `suspense`
- `throwOnError`
- `enabled`
- `placeholderData`

**Returns**

Same object as [useQuery](../reference/useQuery), except for:
- `isPlaceholderData` is missing
- `status` is always `success`
- the derived flags are set accordingly.
5 changes: 2 additions & 3 deletions examples/react/nextjs-suspense-streaming/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use client'
import { useQuery } from '@tanstack/react-query'
import { useSuspenseQuery } from '@tanstack/react-query'
import { Suspense } from 'react'

// export const runtime = "edge"; // 'nodejs' (default) | 'edge'
Expand All @@ -15,7 +15,7 @@ function getBaseURL() {
}
const baseUrl = getBaseURL()
function useWaitQuery(props: { wait: number }) {
const query = useQuery({
const query = useSuspenseQuery({
queryKey: ['wait', props.wait],
queryFn: async () => {
const path = `/api/wait?wait=${props.wait}`
Expand All @@ -29,7 +29,6 @@ function useWaitQuery(props: { wait: number }) {
).json()
return res
},
suspense: true,
})

return [query.data as string, query] as const
Expand Down
4 changes: 2 additions & 2 deletions examples/react/suspense/src/components/Project.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React from 'react'
import { useQuery } from '@tanstack/react-query'
import { useSuspenseQuery } from '@tanstack/react-query'

import Button from './Button'
import Spinner from './Spinner'

import { fetchProject } from '../queries'

export default function Project({ activeProject, setActiveProject }) {
const { data, isFetching } = useQuery({
const { data, isFetching } = useSuspenseQuery({
queryKey: ['project', activeProject],
queryFn: () => fetchProject(activeProject),
})
Expand Down
4 changes: 2 additions & 2 deletions examples/react/suspense/src/components/Projects.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useSuspenseQuery, useQueryClient } from '@tanstack/react-query'

import Button from './Button'
import Spinner from './Spinner'
Expand All @@ -8,7 +8,7 @@ import { fetchProjects, fetchProject } from '../queries'

export default function Projects({ setActiveProject }) {
const queryClient = useQueryClient()
const { data, isFetching } = useQuery({
const { data, isFetching } = useSuspenseQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
})
Expand Down
1 change: 0 additions & 1 deletion examples/react/suspense/src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 0,
suspense: true,
},
},
})
Expand Down
10 changes: 8 additions & 2 deletions packages/query-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,14 +585,20 @@ export interface InfiniteQueryObserverSuccessResult<
status: 'success'
}

export type DefinedInfiniteQueryObserverResult<
TData = unknown,
TError = DefaultError,
> =
| InfiniteQueryObserverRefetchErrorResult<TData, TError>
| InfiniteQueryObserverSuccessResult<TData, TError>

export type InfiniteQueryObserverResult<
TData = unknown,
TError = DefaultError,
> =
| InfiniteQueryObserverLoadingErrorResult<TData, TError>
| InfiniteQueryObserverLoadingResult<TData, TError>
| InfiniteQueryObserverRefetchErrorResult<TData, TError>
| InfiniteQueryObserverSuccessResult<TData, TError>
| DefinedInfiniteQueryObserverResult<TData, TError>

export type MutationKey = readonly unknown[]

Expand Down
5 changes: 1 addition & 4 deletions packages/react-query/src/__tests__/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5422,11 +5422,8 @@ describe('useQuery', () => {
const rendered = renderWithClient(queryClient, <Page />)

await waitFor(() =>
rendered.getByText(
'status: pending, fetchStatus: fetching, failureCount: 1',
),
rendered.getByText(/status: pending, fetchStatus: fetching/i),
)
await waitFor(() => rendered.getByText('failureReason: failed1'))

const onlineMock = mockOnlineManagerIsOnline(false)
window.dispatchEvent(new Event('offline'))
Expand Down
3 changes: 3 additions & 0 deletions packages/react-query/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ export * from './types'
export { useQueries } from './useQueries'
export type { QueriesResults, QueriesOptions } from './useQueries'
export { useQuery } from './useQuery'
export { useSuspenseQuery } from './useSuspenseQuery'
export { useSuspenseInfiniteQuery } from './useSuspenseInfiniteQuery'
export { queryOptions } from './queryOptions'
export { infiniteQueryOptions } from './infiniteQueryOptions'
export {
QueryClientContext,
QueryClientProvider,
Expand Down
93 changes: 93 additions & 0 deletions packages/react-query/src/infiniteQueryOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type { InfiniteData } from '@tanstack/query-core'
import type { UseInfiniteQueryOptions } from './types'
import type { DefaultError, QueryKey } from '@tanstack/query-core'

export type UndefinedInitialDataInfiniteOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
> = UseInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey,
TPageParam
> & {
initialData?: undefined
}

export type DefinedInitialDataInfiniteOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
> = UseInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey,
TPageParam
> & {
initialData: InfiniteData<TQueryData> | (() => InfiniteData<TQueryData>)
}

export function infiniteQueryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
options: UndefinedInitialDataInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryFnData,
TQueryKey,
TPageParam
>,
): UndefinedInitialDataInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryFnData,
TQueryKey,
TPageParam
>

export function infiniteQueryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
options: DefinedInitialDataInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryFnData,
TQueryKey,
TPageParam
>,
): DefinedInitialDataInfiniteOptions<
TQueryFnData,
TError,
TData,
TQueryFnData,
TQueryKey,
TPageParam
>

export function infiniteQueryOptions(options: unknown) {
return options
}
Loading

0 comments on commit d639a0d

Please sign in to comment.