diff --git a/docs/react/guides/suspense.md b/docs/react/guides/suspense.md index 7e944e6b5e..2d8ac8d8f0 100644 --- a/docs/react/guides/suspense.md +++ b/docs/react/guides/suspense.md @@ -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: @@ -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 ( + + + {props.children} + + + ) +} +``` + +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. diff --git a/docs/react/reference/useSuspenseInfiniteQuery.md b/docs/react/reference/useSuspenseInfiniteQuery.md new file mode 100644 index 0000000000..9649a189d7 --- /dev/null +++ b/docs/react/reference/useSuspenseInfiniteQuery.md @@ -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. diff --git a/docs/react/reference/useSuspenseQuery.md b/docs/react/reference/useSuspenseQuery.md new file mode 100644 index 0000000000..c716093699 --- /dev/null +++ b/docs/react/reference/useSuspenseQuery.md @@ -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. diff --git a/examples/react/nextjs-suspense-streaming/src/app/page.tsx b/examples/react/nextjs-suspense-streaming/src/app/page.tsx index dfe62c1c42..7d46a105ae 100644 --- a/examples/react/nextjs-suspense-streaming/src/app/page.tsx +++ b/examples/react/nextjs-suspense-streaming/src/app/page.tsx @@ -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' @@ -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}` @@ -29,7 +29,6 @@ function useWaitQuery(props: { wait: number }) { ).json() return res }, - suspense: true, }) return [query.data as string, query] as const diff --git a/examples/react/suspense/src/components/Project.jsx b/examples/react/suspense/src/components/Project.jsx index 798d09b3fc..a3ffdb4b09 100644 --- a/examples/react/suspense/src/components/Project.jsx +++ b/examples/react/suspense/src/components/Project.jsx @@ -1,5 +1,5 @@ import React from 'react' -import { useQuery } from '@tanstack/react-query' +import { useSuspenseQuery } from '@tanstack/react-query' import Button from './Button' import Spinner from './Spinner' @@ -7,7 +7,7 @@ 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), }) diff --git a/examples/react/suspense/src/components/Projects.jsx b/examples/react/suspense/src/components/Projects.jsx index fad6598aa8..5f652d22a3 100644 --- a/examples/react/suspense/src/components/Projects.jsx +++ b/examples/react/suspense/src/components/Projects.jsx @@ -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' @@ -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, }) diff --git a/examples/react/suspense/src/index.jsx b/examples/react/suspense/src/index.jsx index 1b3622836b..ee00cbca8f 100755 --- a/examples/react/suspense/src/index.jsx +++ b/examples/react/suspense/src/index.jsx @@ -20,7 +20,6 @@ const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 0, - suspense: true, }, }, }) diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index 3658bed99b..27aa5c054c 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -585,14 +585,20 @@ export interface InfiniteQueryObserverSuccessResult< status: 'success' } +export type DefinedInfiniteQueryObserverResult< + TData = unknown, + TError = DefaultError, +> = + | InfiniteQueryObserverRefetchErrorResult + | InfiniteQueryObserverSuccessResult + export type InfiniteQueryObserverResult< TData = unknown, TError = DefaultError, > = | InfiniteQueryObserverLoadingErrorResult | InfiniteQueryObserverLoadingResult - | InfiniteQueryObserverRefetchErrorResult - | InfiniteQueryObserverSuccessResult + | DefinedInfiniteQueryObserverResult export type MutationKey = readonly unknown[] diff --git a/packages/react-query/src/__tests__/useQuery.test.tsx b/packages/react-query/src/__tests__/useQuery.test.tsx index 7c55bce5fc..3b39e80a42 100644 --- a/packages/react-query/src/__tests__/useQuery.test.tsx +++ b/packages/react-query/src/__tests__/useQuery.test.tsx @@ -5422,11 +5422,8 @@ describe('useQuery', () => { const rendered = renderWithClient(queryClient, ) 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')) diff --git a/packages/react-query/src/index.ts b/packages/react-query/src/index.ts index bb69c52e75..8a8f705633 100644 --- a/packages/react-query/src/index.ts +++ b/packages/react-query/src/index.ts @@ -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, diff --git a/packages/react-query/src/infiniteQueryOptions.ts b/packages/react-query/src/infiniteQueryOptions.ts new file mode 100644 index 0000000000..ca138f4836 --- /dev/null +++ b/packages/react-query/src/infiniteQueryOptions.ts @@ -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 | (() => InfiniteData) +} + +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 +} diff --git a/packages/react-query/src/types.ts b/packages/react-query/src/types.ts index 4736d667f9..532e3a9057 100644 --- a/packages/react-query/src/types.ts +++ b/packages/react-query/src/types.ts @@ -2,15 +2,18 @@ import type { DefaultError, + DefinedInfiniteQueryObserverResult, DefinedQueryObserverResult, InfiniteQueryObserverOptions, InfiniteQueryObserverResult, + InfiniteQueryObserverSuccessResult, MutateFunction, MutationObserverOptions, MutationObserverResult, QueryKey, QueryObserverOptions, QueryObserverResult, + QueryObserverSuccessResult, WithRequired, } from '@tanstack/query-core' @@ -35,6 +38,16 @@ export interface UseQueryOptions< 'queryKey' > {} +export interface UseSuspenseQueryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> extends Omit< + UseQueryOptions, + 'enabled' | 'suspense' | 'throwOnError' | 'placeholderData' + > {} + export interface UseInfiniteQueryOptions< TQueryFnData = unknown, TError = DefaultError, @@ -54,6 +67,25 @@ export interface UseInfiniteQueryOptions< 'queryKey' > {} +export interface UseSuspenseInfiniteQueryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +> extends Omit< + UseInfiniteQueryOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey, + TPageParam + >, + 'enabled' | 'suspense' | 'throwOnError' | 'placeholderData' + > {} + export type UseBaseQueryResult< TData = unknown, TError = DefaultError, @@ -64,21 +96,31 @@ export type UseQueryResult< TError = DefaultError, > = UseBaseQueryResult -export type DefinedUseBaseQueryResult< +export type UseSuspenseQueryResult< TData = unknown, TError = DefaultError, -> = DefinedQueryObserverResult +> = Omit, 'isPlaceholderData'> export type DefinedUseQueryResult< TData = unknown, TError = DefaultError, -> = DefinedUseBaseQueryResult +> = DefinedQueryObserverResult export type UseInfiniteQueryResult< TData = unknown, TError = DefaultError, > = InfiniteQueryObserverResult +export type DefinedUseInfiniteQueryResult< + TData = unknown, + TError = DefaultError, +> = DefinedInfiniteQueryObserverResult + +export type UseSuspenseInfiniteQueryResult< + TData = unknown, + TError = DefaultError, +> = Omit, 'isPlaceholderData'> + export interface UseMutationOptions< TData = unknown, TError = DefaultError, diff --git a/packages/react-query/src/useInfiniteQuery.ts b/packages/react-query/src/useInfiniteQuery.ts index a320ccfd52..8b4e7df438 100644 --- a/packages/react-query/src/useInfiniteQuery.ts +++ b/packages/react-query/src/useInfiniteQuery.ts @@ -8,9 +8,52 @@ import type { QueryKey, QueryObserver, } from '@tanstack/query-core' -import type { UseInfiniteQueryOptions, UseInfiniteQueryResult } from './types' +import type { + DefinedUseInfiniteQueryResult, + UseInfiniteQueryOptions, + UseInfiniteQueryResult, +} from './types' +import type { + DefinedInitialDataInfiniteOptions, + UndefinedInitialDataInfiniteOptions, +} from './infiniteQueryOptions' + +export function useInfiniteQuery< + TQueryFnData, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +>( + options: UndefinedInitialDataInfiniteOptions< + TQueryFnData, + TError, + TData, + TQueryFnData, + TQueryKey, + TPageParam + >, + queryClient?: QueryClient, +): UseInfiniteQueryResult + +export function useInfiniteQuery< + TQueryFnData, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +>( + options: DefinedInitialDataInfiniteOptions< + TQueryFnData, + TError, + TData, + TQueryFnData, + TQueryKey, + TPageParam + >, + queryClient?: QueryClient, +): DefinedUseInfiniteQueryResult -// HOOK export function useInfiniteQuery< TQueryFnData, TError = DefaultError, diff --git a/packages/react-query/src/useQuery.ts b/packages/react-query/src/useQuery.ts index b25d049c73..0a673b9a6f 100644 --- a/packages/react-query/src/useQuery.ts +++ b/packages/react-query/src/useQuery.ts @@ -12,8 +12,6 @@ import type { UndefinedInitialDataOptions, } from './queryOptions' -// HOOK - export function useQuery< TQueryFnData = unknown, TError = DefaultError, diff --git a/packages/react-query/src/useSuspenseInfiniteQuery.ts b/packages/react-query/src/useSuspenseInfiniteQuery.ts new file mode 100644 index 0000000000..61450eb463 --- /dev/null +++ b/packages/react-query/src/useSuspenseInfiniteQuery.ts @@ -0,0 +1,47 @@ +'use client' +import { InfiniteQueryObserver } from '@tanstack/query-core' +import { useBaseQuery } from './useBaseQuery' +import type { + InfiniteQueryObserverSuccessResult, + QueryObserver, +} from '@tanstack/query-core' +import type { + DefaultError, + InfiniteData, + QueryClient, + QueryKey, +} from '@tanstack/query-core' +import type { + UseSuspenseInfiniteQueryOptions, + UseSuspenseInfiniteQueryResult, +} from './types' + +export function useSuspenseInfiniteQuery< + TQueryFnData, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +>( + options: UseSuspenseInfiniteQueryOptions< + TQueryFnData, + TError, + TData, + TQueryFnData, + TQueryKey, + TPageParam + >, + queryClient?: QueryClient, +): UseSuspenseInfiniteQueryResult { + return useBaseQuery( + { + ...options, + enabled: true, + suspense: true, + throwOnError: true, + }, + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + InfiniteQueryObserver as typeof QueryObserver, + queryClient, + ) as InfiniteQueryObserverSuccessResult +} diff --git a/packages/react-query/src/useSuspenseQuery.ts b/packages/react-query/src/useSuspenseQuery.ts new file mode 100644 index 0000000000..1b25892542 --- /dev/null +++ b/packages/react-query/src/useSuspenseQuery.ts @@ -0,0 +1,26 @@ +'use client' +import { QueryObserver } from '@tanstack/query-core' +import { useBaseQuery } from './useBaseQuery' +import type { UseSuspenseQueryOptions, UseSuspenseQueryResult } from './types' +import type { DefaultError, QueryClient, QueryKey } from '@tanstack/query-core' + +export function useSuspenseQuery< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: UseSuspenseQueryOptions, + queryClient?: QueryClient, +): UseSuspenseQueryResult { + return useBaseQuery( + { + ...options, + enabled: true, + suspense: true, + throwOnError: true, + }, + QueryObserver, + queryClient, + ) as UseSuspenseQueryResult +}