import {
Hydrate,
type HydrateProps,
type OmitKeyof,
QueryClient,
type QueryOptions,
type UseInfiniteQueryOptions,
type WithRequired,
dehydrate,
} from '@tanstack/react-query'
import type { ReactNode } from 'react'
import { ClientOnly } from './components/ClientOnly'
/**
* A server component that fetches multiple queries on the server and hydrates them to the client.
*
* @experimental This component is experimental and may be changed or removed in the future.
*
* @description
* QueriesHydration is designed for React Server Components (RSC).
* It pre-fetches multiple queries on the server side and automatically hydrates
* the data to the client, enabling seamless data synchronization between server and client.
*
* When errors occur during server-side fetching, the component gracefully falls back
* to client-side rendering, ensuring your application remains resilient.
*
* @example
* ```tsx
* // app/page.tsx (Server Component)
* import { Suspense } from 'react'
* import { QueriesHydration } from '@suspensive/react-query'
* import { queryOptions } from '@tanstack/react-query'
*
* const userQueryOptions = (userId: string) => queryOptions({
* queryKey: ['user', userId],
* queryFn: () => fetchUser(userId)
* })
*
* const postsQueryOptions = () => queryOptions({
* queryKey: ['posts'],
* queryFn: () => fetchPosts()
* })
*
* export default function Page({ userId }: { userId: string }) {
* return (
* <>
* Loading user...}>
*
*
*
*
*
* Loading posts...}>
*
*
*
*
* >
* )
* }
* ```
*
* @example
* ```tsx
* // With custom error fallback
* Loading user...}>
* Fetching on client... }}
* >
*
*
*
* ```
*
* @see {@link https://suspensive.org/docs/react-query/QueriesHydration Documentation}
*/
export async function QueriesHydration({
queries,
children,
queryClient = new QueryClient(),
skipSsrOnError = true,
timeout,
...props
}: {
/**
* The QueryClient instance to use for fetching queries.
*/
queryClient?: QueryClient
/**
* An array of query options or infinite query options to be fetched on the server. Each query must include a `queryKey`.
* You can mix regular queries and infinite queries in the same array.
*/
queries: (
| WithRequired, 'queryKey'>
| WithRequired, 'queryKey'>
)[]
/**
* Controls error handling behavior:
* - `true` (default): Skips SSR and falls back to client-side rendering when server fetch fails
* - `false`: Proceeds with SSR without hydration (retry fetching on client component server rendering)
* - `{ fallback: ReactNode }`: Skips SSR with custom fallback UI during client-side rendering
*/
skipSsrOnError?:
| boolean
| {
fallback: ReactNode
}
/**
* The timeout in milliseconds for the query.
* If the query takes longer than the timeout, it will be considered as an error.
* When not set, no timeout is applied.
*/
timeout?: number
} & OmitKeyof) {
const timeoutController = timeout != null && timeout >= 0 ? createTimeoutController(timeout) : undefined
try {
const queriesPromise = Promise.all(
queries.map((query) =>
'getNextPageParam' in query ? queryClient.fetchInfiniteQuery(query) : queryClient.fetchQuery(query)
)
)
await (timeoutController != null ? Promise.race([queriesPromise, timeoutController.promise]) : queriesPromise)
timeoutController?.clear()
} catch {
timeoutController?.clear()
queries.forEach((query) => void queryClient.cancelQueries(query))
if (skipSsrOnError) {
return (
{children}
)
}
}
return (
{children}
)
}
const createTimeoutController = (ms: number) => {
let timerId: ReturnType | undefined
return {
promise: new Promise((_, reject) => {
timerId = setTimeout(() => reject(new Error(`QueriesHydration: timeout after ${ms} ms`)), ms)
}),
clear: () => timerId != null && clearTimeout(timerId),
}
}