Capítulo 53 de 57

Chapter 53: TanStack Query Integration

Core Idea

The @tanstack/react-router-ssr-query package wires TanStack Query into TanStack Router's SSR pipeline, automating QueryClient dehydration/hydration, streaming queries that resolve during the initial server render, and handling redirect() thrown from queries or mutations.

Key Concepts

  • setupRouterSsrQueryIntegration({ router, queryClient }): The core setup call that wires the router and query client together for automatic SSR dehydration/hydration and streaming.
  • Per-request QueryClient: In SSR environments, a fresh QueryClient must be created per request (typically inside a getRouter() factory function) to avoid cross-request state leakage.
  • wrapQueryClient: Defaults to wrapping the app with QueryClientProvider automatically; set to false if you already provide your own provider.
  • dehydrateOptions / hydrateOptions: Customize SSR serialization, e.g. shouldDehydrateQuery to exclude certain queries, or hydrateOptions.defaultOptions.queries.gcTime to control cache lifetime post-hydration.
  • useSuspenseQuery vs useQuery: useSuspenseQuery executes on the server during SSR and streams to the client as it resolves; plain useQuery never runs server-side, it only fetches after client hydration.
  • Loader prefetching pattern: Calling context.queryClient.ensureQueryData(queryOptions) in a route's loader avoids client-side fetch waterfalls; the component then reads the same query via useSuspenseQuery.
  • Non-blocking prefetch/streaming: Calling queryClient.fetchQuery(...) in a loader without awaiting or returning the promise starts the query on the server and streams the result to the client without blocking the SSR response; awaiting/returning it blocks SSR until it resolves.
  • handleRedirects: Enabled by default; intercepts a redirect() thrown from a query/mutation and performs a client-side router navigation. Disable with handleRedirects: false for custom handling.
  • TanStack Start compatibility: TanStack Start uses TanStack Router internally, so this integration's setup and streaming behavior apply unchanged there.

Code Examples

import { QueryClient } from '@tanstack/react-query'
import { createRouter } from '@tanstack/react-router'
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
import { routeTree } from './routeTree.gen'

export function getRouter() {
  const queryClient = new QueryClient()
  const router = createRouter({
    routeTree,
    context: { queryClient },
    scrollRestoration: true,
    defaultPreload: 'intent',
  })

  setupRouterSsrQueryIntegration({ router, queryClient })

  return router
}
  • What it demonstrates: Standard setup creating a per-request router+queryClient pair and wiring the SSR/streaming integration.
const postsQuery = queryOptions({
  queryKey: ['posts'],
  queryFn: () => fetch('/api/posts').then((r) => r.json()),
})

export const Route = createFileRoute('/posts')({
  loader: ({ context }) => context.queryClient.ensureQueryData(postsQuery),
  component: PostsPage,
})

function PostsPage() {
  const { data } = useSuspenseQuery(postsQuery)
  return <div>{data.map((p: any) => p.title).join(', ')}</div>
}
  • What it demonstrates: Preloading a query in loader and reading it via useSuspenseQuery for SSR + streaming without a loading flash.

Key Takeaways

  1. Create a fresh QueryClient per SSR request via a getRouter() factory, never a module-level singleton, to avoid leaking state between users.
  2. Use useSuspenseQuery (not plain useQuery) for data that should participate in SSR and streaming; reserve useQuery for client-only data.
  3. In loaders, choose deliberately between blocking (await/return the fetch) and non-blocking (fire-and-forget) prefetching depending on whether the route should wait for that data before responding.
  4. Redirects thrown from Query queries/mutations are handled automatically by default, no manual try/catch + navigate() needed.

Connects To

  • Ch 49: Router Context, this integration typically injects queryClient via router context so loaders can access it.
  • Ch 50: Not Found Errors, both notFound() and redirect() are throwable primitives; this integration specifically automates handling redirect() from Query.