Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react-query): useSuspenseQuery #5739

Merged
merged 17 commits into from
Jul 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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).
TkDodo marked this conversation as resolved.
Show resolved Hide resolved

## 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