Capítulo 37 de 80

Chapter 37: Advanced Server Rendering

Core Idea

React Server Components are "just another loader phase" from React Query's perspective — prefetch in a Server Component, dehydrate, hand off to a HydrationBoundary Client Component — but with streaming, pending (not just resolved) queries can also be dehydrated so a Suspense boundary doesn't have to block on the full await.

Key Concepts

  • Terminology trap: "server"/"client" (React Query concepts) don't map 1:1 to Server/Client Components — Client Components can still run during the initial server-rendering pass; only Server Components are guaranteed server-only, on both first load and page transitions.
  • Per-environment QueryClient factory: use environmentManager.isServer() to always create a fresh client on the server, but reuse a module-level singleton in the browser (created lazily, guarded against being recreated if React suspends before a boundary exists) — this is the standard getQueryClient() pattern for app-router setups.
  • Nesting Server Components: each Server Component can prefetch its own slice of data into its own QueryClient and wrap its own <HydrationBoundary> — multiple boundaries in one tree are fine. Sequential awaits across nested Server Components still create a real server-side waterfall unless the framework's routing (e.g. Next.js layouts/parallel routes) fetches them in parallel automatically.
  • Shared vs. per-component QueryClient for prefetching: a single request-scoped client (via React's cache()) avoids re-creating clients everywhere, at the cost of re-serializing the entire client (including already-dehydrated queries) on every dehydrate() call — worth it mainly when the underlying fetcher doesn't already dedupe requests the way Next.js's fetch() does.
  • Data ownership / revalidation hazard: rendering the same query's data directly in a Server Component (e.g. posts.length) and also via useQuery in a Client Component risks the two going out of sync once the client-side query revalidates — React Query has no way to re-trigger the Server Component's render. Either accept staleTime: Infinity for that data, or treat Server Components strictly as a prefetch phase and never render queryClient.query()'s result directly.
  • Streaming pending queries: since v5.40, dehydrate can include still-pending queries (not just resolved ones) via shouldDehydrateQuery including query.state.status === 'pending' — combined with getQueryClient() not being awaited before render, this streams a Promise to the client that useSuspenseQuery picks up directly, letting prefetches that shouldn't block a whole boundary (e.g. page 2 of an infinite list) stream in as they resolve. useQuery (not useSuspenseQuery) still picks up the streamed promise correctly, but won't suspend the boundary — it renders pending and opts out of the server-rendered content for that piece.
  • Custom serialization for streaming: dehydrate.serializeData/hydrate.deserializeData let a non-JSON return type (e.g. Temporal datetime objects) survive the streamed hydration boundary via a shared transformer.
  • Persist adapter + streaming caution: never let the persist adapter attempt to save an unresolved Promise to storage — scope dehydrateOptions.shouldDehydrateQuery to defaultShouldDehydrateQuery (successful queries only) when combining persistence with streamed pending queries.
  • Prefetch-less alternative (@tanstack/react-query-next-experimental): ReactQueryStreamedHydration lets you skip manual prefetching entirely and just call useSuspenseQuery inside Client Components, streamed via Suspense — much simpler DX, but the flattening benefit only applies to the initial page load; client-side navigations regress to the full un-flattened waterfall (worse than getServerSideProps, which could at least parallelize data and code).
  • Server Actions caveat: don't use Next.js Server Actions as a queryFn — client-invoked Server Actions run serially (not in parallel), which conflicts with how React Query fetches/refetches, and passing a Server Action by reference to queryFn can also fail outright. Server Actions remain a good fit for mutations, just not for reads.

Key Takeaways

  1. Treat Server Components strictly as a prefetch phase — avoid rendering queryClient.query()'s return value directly alongside a Client Component reading the same query, or the two will drift once the client revalidates.
  2. Reach for streaming pending queries (v5.40+) specifically to avoid a Suspense boundary blocking on data that isn't actually critical to that boundary's content.
  3. Prefer the prefetch-based approach over the prefetch-less experimental package whenever subsequent client-side navigation performance matters, not just the very first load.

Connects To

  • Server Rendering & Hydration: the base pipeline (dehydrate/HydrationBoundary) this chapter extends into Server Components and streaming.
  • Suspense: useSuspenseQuery's role in picking up a streamed pending query.
  • persistQueryClient: the persistence caveat when combined with streaming.