QueriesHydration
<QueriesHydration/> is an experimental feature, so this interface may change.
A component that prefetches multiple queries on the server and automatically hydrates them to client components.
This is useful when you want to prefetch data on the server side and pass it to the client in React Server Components environments.
💡 Recommended reading: Before using this component, we recommend reading the TanStack Query Advanced Server Rendering guide , which covers Server Components, streaming, and the Next.js App Router. Understanding these concepts will help you use
QueriesHydrationmore effectively.
Basic Usage
page.tsx (RSC)
import { QueriesHydration } from '@suspensive/react-query'
import { Suspense } from '@suspensive/react'
import { userQueryOptions, postsQueryOptions } from './queries'
import { UserProfile, PostList } from './_components'
// Server Component
const PostsPage = ({ userId }: { userId: number }) => {
return (
<>
<Suspense fallback={<div>Loading user...</div>}>
<QueriesHydration queries={[userQueryOptions(userId)]}>
<UserProfile userId={userId} />
</QueriesHydration>
</Suspense>
<Suspense fallback={<div>Loading posts...</div>}>
<QueriesHydration queries={[postsQueryOptions(userId)]}>
<PostList userId={userId} />
</QueriesHydration>
</Suspense>
</>
)
}Props
queries
An array of queries to be prefetched on the server. Each query must have a queryKey.
type QueriesHydrationProps = {
queries: WithRequired<QueryOptions<any, any, any, any>, 'queryKey'>[]
children: ReactNode
queryClient?: QueryClient // Optional (default: new QueryClient())
skipSsrOnError?: boolean | { fallback: ReactNode } // Optional (default: true)
}queryClient
Optionally, you can pass a QueryClient instance to use. If not provided, a new QueryClient instance will be created.
import { QueryClient } from '@tanstack/react-query'
import { QueriesHydration } from '@suspensive/react-query'
const queryClient = new QueryClient()
const Page = async () => {
return (
<QueriesHydration queryClient={queryClient} queries={[]}>
{/* ... */}
</QueriesHydration>
)
}skipSsrOnError
Controls the behavior when an error occurs while fetching queries on the server. This option becomes clearer when you understand the 3 stages where data fetching occurs:
- RSC (React Server Component): Query execution in
QueriesHydration - RCC (React Client Component) - Server: Query execution in
useSuspenseQueryif no cached data exists - RCC (React Client Component) - Browser: Query execution in
useSuspenseQueryif no cached or fresh data exists
If stage 1 fails, stage 2 is likely to fail for the same reason. However, stage 3 (browser) might succeed (e.g., authentication-related issues, etc.). Since the browser can retry and potentially succeed, it’s better to use skipSsrOnError to skip RCC(server) and fetch directly from RCC(browser) when RSC fails.
Important: When stage 1 succeeds (no error), the data is already hydrated to the client. In this case, you can verify in the browser’s Network tab that no additional fetch requests are made in stages 2 and 3, as the data is served from the cache. This demonstrates the efficiency of server-side prefetching with QueriesHydration.
true(default): If stage 1 fails, skip SSR and retry in stage 3 (browser)false: Even if stage 1 fails, proceed to stage 2 without hydration (retry fetching on server){ fallback: ReactNode }: If stage 1 fails, skip SSR and display a custom fallback UI while moving to stage 3
import { QueriesHydration } from '@suspensive/react-query'
import { Suspense } from '@suspensive/react'
import {
userQueryOptions,
postsQueryOptions,
commentsQueryOptions,
} from './queries'
import { UserProfile, PostList, CommentList } from './_components'
const Page = async ({ userId }: { userId: number }) => {
return (
<>
{/* Skip SSR and retry in browser if stage 1 fails (default behavior) */}
<Suspense fallback={<div>Loading...</div>}>
<QueriesHydration queries={[userQueryOptions(userId)]}>
<UserProfile />
</QueriesHydration>
</Suspense>
{/* Skip SSR and retry in browser with custom fallback if stage 1 fails */}
<Suspense fallback={<div>Loading...</div>}>
<QueriesHydration
queries={[postsQueryOptions()]}
skipSsrOnError={{
fallback: <div>Unable to fetch data from server...</div>,
}}
>
<PostList />
</QueriesHydration>
</Suspense>
{/* Retry in stage 2 (RCC server) even if stage 1 fails */}
<Suspense fallback={<div>Loading...</div>}>
<QueriesHydration
queries={[commentsQueryOptions()]}
skipSsrOnError={false}
>
<CommentList />
</QueriesHydration>
</Suspense>
</>
)
}Motivation: Simplify prefetching multiple queries in Server Components
In React Server Components environments, you can prefetch data on the server to eliminate initial loading states. However, manually prefetching and dehydrating multiple queries is cumbersome.
Traditional approach: Manual prefetch and dehydrate
page.tsx (RSC)
import {
QueryClient,
dehydrate,
HydrationBoundary,
} from '@tanstack/react-query'
import { Suspense } from '@suspensive/react'
import { userQueryOptions, postsQueryOptions } from './queries'
import { UserProfile, PostList } from './_components'
// Server Component
const PostsPage = ({ userId }: { userId: number }) => {
return (
<>
<Suspense fallback={<div>Loading user...</div>}>
<UserProfileWithData userId={userId} />
</Suspense>
<Suspense fallback={<div>Loading posts...</div>}>
<PostListWithData userId={userId} />
</Suspense>
</>
)
}
// Need to separate each server component for HTML Streaming
const UserProfileWithData = async ({ userId }: { userId: number }) => {
const queryClient = new QueryClient()
try {
await queryClient.ensureQueryData(userQueryOptions(userId))
} catch (error) {
return (
// If queryClient.ensureQueryData fails, use ClientOnly to prevent SSR and render on the browser directly
<ClientOnly fallback={<div>Loading user...</div>}>
<UserProfile userId={userId} />
</ClientOnly>
)
}
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<UserProfile userId={userId} />
</HydrationBoundary>
)
}
const PostListWithData = async ({ userId }: { userId: number }) => {
const queryClient = new QueryClient()
try {
await queryClient.ensureQueryData(postsQueryOptions(userId))
} catch (error) {
return (
// If queryClient.ensureQueryData fails, use ClientOnly to prevent SSR and render on the browser directly
<ClientOnly fallback={<div>Loading posts...</div>}>
<PostList userId={userId} />
</ClientOnly>
)
}
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<PostList userId={userId} />
</HydrationBoundary>
)
}Using QueriesHydration
With <QueriesHydration/>, all of this is handled automatically:
page.tsx (RSC)
import { QueriesHydration } from '@suspensive/react-query'
import { Suspense } from '@suspensive/react'
import { userQueryOptions, postsQueryOptions } from './queries'
import { UserProfile, PostList } from './_components'
// Server Component
const PostsPage = ({ userId }: { userId: number }) => {
return (
<>
<Suspense fallback={<div>Loading user...</div>}>
<QueriesHydration queries={[userQueryOptions(userId)]}>
<UserProfile userId={userId} />
</QueriesHydration>
</Suspense>
<Suspense fallback={<div>Loading posts...</div>}>
<QueriesHydration queries={[postsQueryOptions(userId)]}>
<PostList userId={userId} />
</QueriesHydration>
</Suspense>
</>
)
}Key benefits:
- Concise code: Automates QueryClient creation, prefetch, and dehydrate processes
- Parallel data fetching: Uses
Promise.allto process all queries in parallel - Type safety: Requires queryKey to prevent mistakes
- Consistent pattern: Handles multiple queries consistently
Dependent Queries and Streaming
When handling dependent queries, wrapping each component with a separate QueriesHydration maximizes streaming benefits by creating independent Suspense boundaries. By sharing the same queryClient, you can use the result of the first query in subsequent queries.
page.tsx (RSC)
import { QueryClient } from '@tanstack/react-query'
import { QueriesHydration } from '@suspensive/react-query'
import { Suspense } from '@suspensive/react'
import {
productQueryOptions,
productReviewsQueryOptions,
relatedProductsQueryOptions,
} from './queries'
import { ProductInfo, ProductReviews, RelatedProducts } from './_components'
const ProductPage = async ({ productId }: { productId: string }) => {
const queryClient = new QueryClient()
// 1. First, fetch product information
const product = await queryClient.ensureQueryData(
productQueryOptions(productId)
)
return (
<>
{/* Product information */}
<Suspense fallback={<div>Loading product...</div>}>
<QueriesHydration
queryClient={queryClient}
queries={[productQueryOptions(productId)]}
>
<ProductInfo productId={productId} />
</QueriesHydration>
</Suspense>
{/* Product reviews: depends on product information (e.g., filtered by product category) */}
<Suspense fallback={<div>Loading reviews...</div>}>
<QueriesHydration
queryClient={queryClient}
queries={[productReviewsQueryOptions(productId)]}
>
<ProductReviews productId={productId} />
</QueriesHydration>
</Suspense>
{/* Related products: depends on product information (same category) */}
<Suspense fallback={<div>Loading related products...</div>}>
<QueriesHydration
queryClient={queryClient}
queries={[relatedProductsQueryOptions(product.categoryId)]}
>
<RelatedProducts categoryId={product.categoryId} />
</QueriesHydration>
</Suspense>
</>
)
}Benefits of this pattern:
- Independent Suspense boundaries: Each component streams independently, so
ProductInforenders first when ready - Shared queryClient: Cache is shared, allowing reuse of
productdata - Dependent queries support: You can pass
product.categoryIdtorelatedProductsQueryOptions - Progressive rendering: Components stream sequentially as they become ready
Example
Here’s a live example demonstrating QueriesHydration with Next.js streaming:

- Live Demo : See it in action
- Source Code : View the full implementation
Tip: In the live demo, check the “4. no error (Best Practice)” case and open the browser’s Developer Tools Network tab. You’ll notice that no fetch requests are made because the data was successfully prefetched on the server and hydrated to the client, demonstrating the efficiency of QueriesHydration.
Disabling SSR
If you want to skip server-side rendering for a specific component, simply add the clientOnly prop to <Suspense/>. In this case, since you don’t need to prefetch data on the server, you don’t need QueriesHydration either:
import { QueriesHydration } from '@suspensive/react-query'
import { Suspense } from '@suspensive/react'
import { userQueryOptions, postsQueryOptions } from './queries'
import { UserProfile, PostList } from './_components'
const PostsPage = ({ userId }: { userId: number }) => {
return (
<>
{/* UserProfile skips SSR and only renders on the client */}
<Suspense clientOnly fallback={<div>Loading user...</div>}>
<UserProfile userId={userId} />
</Suspense>
{/* PostList is prefetched and rendered on the server */}
<Suspense fallback={<div>Loading posts...</div>}>
<QueriesHydration queries={[postsQueryOptions(userId)]}>
<PostList userId={userId} />
</QueriesHydration>
</Suspense>
</>
)
}When using the clientOnly prop:
- Components within that Suspense boundary will not be rendered on the server
- Data will only be fetched and rendered on the client
- You don’t need
QueriesHydrationsince server-side prefetch is not needed
Version Differences
@tanstack/react-query v5
In @tanstack/react-query v5, it uses the HydrationBoundary component.
import { HydrationBoundary } from '@tanstack/react-query'
import { QueriesHydration } from '@suspensive/react-query'
// QueriesHydration internally uses HydrationBoundary@tanstack/react-query v4
In @tanstack/react-query v4, it uses the Hydrate component.
import { Hydrate } from '@tanstack/react-query'
import { QueriesHydration } from '@suspensive/react-query'
// QueriesHydration internally uses HydrateImportant Notes
- This component is an async server component.
- It can only be used in frameworks that support React Server Components (Next.js 13+ App Router, etc.).
- All queries must include a
queryKey.
Version History
| Version | Changes |
|---|---|
| v3.14.0 | <QueriesHydration/> has been added as an experimental feature. |