Skip to Content

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 QueriesHydration more effectively.

Basic Usage

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:

  1. RSC (React Server Component): Query execution in QueriesHydration
  2. RCC (React Client Component) - Server: Query execution in useSuspenseQuery if no cached data exists
  3. RCC (React Client Component) - Browser: Query execution in useSuspenseQuery if 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

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:

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:

  1. Concise code: Automates QueryClient creation, prefetch, and dehydrate processes
  2. Parallel data fetching: Uses Promise.all to process all queries in parallel
  3. Type safety: Requires queryKey to prevent mistakes
  4. 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.

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:

  1. Independent Suspense boundaries: Each component streams independently, so ProductInfo renders first when ready
  2. Shared queryClient: Cache is shared, allowing reuse of product data
  3. Dependent queries support: You can pass product.categoryId to relatedProductsQueryOptions
  4. Progressive rendering: Components stream sequentially as they become ready

Example

Here’s a live example demonstrating QueriesHydration with Next.js streaming:

Next.js Streaming React Query Example

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 QueriesHydration since 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 Hydrate

Important 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

VersionChanges
v3.14.0<QueriesHydration/> has been added as an experimental feature.
Last updated on