Choose blocking or streaming queries, prevent cross-user cache leakage, and verify hydration does not immediately duplicate requests.
One QueryClient per request
A module-level QueryClient on the server can share cache across requests and users. Create it inside getRouter, then pass the same instance to Router Context and setupRouterSsrQueryIntegration. The integration owns initial dehydration, client hydration, and subsequent query-result streaming.
import { QueryClient } from '@tanstack/react-query'
import { createRouter } from '@tanstack/react-router'
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
export function getRouter() {
const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: 30_000 } },
})
const router = createRouter({
routeTree,
context: { queryClient },
})
setupRouterSsrQueryIntegration({ router, queryClient })
return router
}Choose what blocks and what streams
Returning or awaiting ensureQueryData in a loader makes critical data available before route entry. Starting fetchQuery without awaiting lets the result stream during initial rendering. useSuspenseQuery participates in SSR and streaming, while plain useQuery begins on the client after hydration. Choose by page value instead of blocking on every request.
SSR payloads are visible to the browser
Dehydrated cache enters HTML or streamed responses, so it must not contain unauthorized data, secrets, or server-only fields. Custom serialization must escape the HTML context correctly; prefer the framework integration over concatenating JSON.stringify output into a script.
Turn this lesson into a verifiable skill
Every SSR request receives an isolated QueryClient, critical queries hydrate, optional queries can stream, and hydration does not immediately refetch.
Block on the issue summary, stream the activity feed, and use two concurrent sessions to prove cached data never crosses users.
Inspect server logs, HTML/stream payload, and the browser network panel; initial data fetches once, serialized state excludes private fields, and two users see isolated data.
- Reusing a global module-level QueryClient on the server.
- Dehydrating secrets or unauthorized fields into a browser-visible payload.