Execution contract
- Use when
- Use when a route should warm Query data before entry and the component will observe the same cache entry.
- Avoid when
- Do not block a loader on expensive, rarely visited, or non-critical data.
- Preconditions
- Create a shared queryOptions factory and include every route input affecting the query key in loader deps.
- Verification
- Navigation issues one request, the component hits the same cache, and parameter changes create a distinct predictable key.
- Failure mode
- If the component refetches immediately, compare full query keys, freshness settings, and QueryClient identity.
- Security
- Prefetching does not hide responses; never place unauthorized data in serialized cache or SSR payloads.
Required integration
Create QueryClient inside getRouter, expose it through Router context, then call setupRouterSsrQueryIntegration. getRouter runs per SSR request; do not hoist QueryClient into a module singleton.
const queryClient = new QueryClient()
const router = createRouter({
routeTree,
context: { queryClient },
})
setupRouterSsrQueryIntegration({ router, queryClient })Blocking critical data
For render-critical data, return the ensureQueryData promise from the loader. Call useSuspenseQuery with the same queryOptions in the component. Router waits for navigation, Query owns cache, and the SSR integration handles dehydration and hydration.
const issuesQuery = queryOptions({
queryKey: ['issues'],
queryFn: getIssues,
})
export const Route = createFileRoute('/issues/')({
loader: ({ context }) =>
context.queryClient.ensureQueryData(issuesQuery),
component: () => {
const { data } = useSuspenseQuery(issuesQuery)
return <IssueList issues={data} />
},
})Verification checklist
Confirm root Router context types QueryClient, no QueryClient is shared across SSR requests, loader and component use an identical queryKey, and the hydrated page does not repeat its initial browser request.