Capítulo 36 de 80
Server rendering turns the client's markup → JS → query waterfall into markup-with-data → JS by prefetching on the server, dehydrating that state into serializable JSON embedded in the page, and hydrating it back into a fresh client-side QueryClient — the full dehydrate/HydrationBoundary pipeline, not the simpler-but-flawed initialData shortcut.
QueryClient: always construct the QueryClient inside the app (in React state or a ref), never at module scope — a module-scoped client is shared across every request/user on the server, leaking data between users.initialData: passing server-fetched data as initialData to useQuery works with zero extra APIs, but has real drawbacks — it must be threaded down to wherever useQuery is actually called, it silently never overwrites data already in the cache (even if the new value is fresher), and there's no real dataUpdatedAt since it's stamped at page-load time rather than server-fetch time.await queryClient.query(...) (parallelize with Promise.all where possible — it's fine to leave some queries unprefetched, they'll just fetch client-side after hydration), return dehydrate(queryClient), and wrap the rendered tree in <HydrationBoundary state={dehydratedState}>. Three separate QueryClient instances are involved end to end: the loader's preload client, the server-render client, and the client-side client — the dehydrated payload seeds the latter two identically so they render the same markup.awaits in the loader before the final dehydrate(queryClient) call.dehydrate() only includes successful queries by default; errors from void queryClient.query(...).catch(noop) are swallowed so the client retries them normally, and the server-rendered output shows a loading state for that piece instead of failing the whole page. For genuinely critical data, await without swallowing the error and handle it explicitly (404/500) instead.undefined, Error, Date, Map, Set, BigInt, Infinity/NaN/-0, or regexes in query data, unless a richer serializer (e.g. superjson) is used. In a custom SSR setup, never JSON.stringify the dehydrated state directly into markup — that's an XSS vector; use an escaping-safe serializer (Serialize JavaScript, devalue) instead.dataUpdatedAt reflects the server's fetch time (in UTC, so timezones don't matter) — since staleTime defaults to 0, a page will double-fetch on load unless a higher staleTime is set. This pairs well with CDN-cached markup: cache the page for a long time, but set query staleTime shorter so the background refetch keeps data current without re-rendering server-side.gcTime defaults to Infinity on the server and is auto-cleared once a request completes — if you override it to a finite value, you own cleanup (e.g. queryClient.clear() after dehydration). Never set gcTime: 0 server-side — garbage collection can race the hydration boundary reading the data, causing a hydration error; use something like 2 * 1000 if a short gcTime is genuinely needed.// Loader: prefetch, then dehydrate
export async function getStaticProps() {
const queryClient = new QueryClient()
await queryClient.query({ queryKey: ['posts'], queryFn: getPosts }).catch(noop)
return { props: { dehydratedState: dehydrate(queryClient) } }
}
// Route: hydrate the prefetched state back into the client cache
export default function PostsRoute({ dehydratedState }) {
return (
<HydrationBoundary state={dehydratedState}>
<Posts />
</HydrationBoundary>
)
}
HydrationBoundary round trip that avoids initialData's drawbacks.dehydrate/HydrationBoundary pipeline over initialData for anything beyond a quick prototype — initialData silently never overwrites fresher cache data, which breaks repeated navigation to the same page.QueryClient at module scope in an SSR app — it must be created per-request, inside the component tree.staleTime for SSR'd queries deliberately — the staleTime: 0 default causes an immediate client-side refetch on every page load, which usually isn't what you want when the markup already has fresh data.gcTime/clear(), central to the server-memory caveat here.