This integration automates SSR dehydration/hydration and streaming between TanStack Router and TanStack Query. If you haven't read the standard External Data Loading guide, start there.
The TanStack query integration is a separate package that you need to install:
npm install @tanstack/react-router-ssr-query # or pnpm add @tanstack/react-router-ssr-query # or yarn add @tanstack/react-router-ssr-query # or bun add @tanstack/react-router-ssr-query npm install @tanstack/react-router-ssr-query # or pnpm add @tanstack/react-router-ssr-query # or yarn add @tanstack/react-router-ssr-query # or bun add @tanstack/react-router-ssr-query Create your router and wire up the integration. Ensure a fresh QueryClient is created per request in SSR environments.
// src/router.tsx 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, // optionally expose the QueryClient via router context context: { queryClient }, scrollRestoration: true, defaultPreload: 'intent', }) setupRouterSsrQueryIntegration({ router, queryClient, // optional: // handleRedirects: true, // wrapQueryClient: true, }) return router } // src/router.tsx 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, // optionally expose the QueryClient via router context context: { queryClient }, scrollRestoration: true, defaultPreload: 'intent', }) setupRouterSsrQueryIntegration({ router, queryClient, // optional: // handleRedirects: true, // wrapQueryClient: true, }) return router } By default, the integration wraps your router with a QueryClientProvider. If you already provide your own provider, pass wrapQueryClient: false and keep your custom wrapper.
// Suspense: executes on server and streams const { data } = useSuspenseQuery(postsQuery) // Non-suspense: executes only on client const { data, isLoading } = useQuery(postsQuery) // Suspense: executes on server and streams const { data } = useSuspenseQuery(postsQuery) // Non-suspense: executes only on client const { data, isLoading } = useQuery(postsQuery) Preload critical data in the route loader to avoid waterfalls and loading flashes, then read it in the component. The integration ensures server-fetched data is dehydrated and streamed to the client during SSR.
// src/routes/posts.tsx import { queryOptions, useSuspenseQuery, useQuery } from '@tanstack/react-query' import { createFileRoute } from '@tanstack/react-router' const postsQuery = queryOptions({ queryKey: ['posts'], queryFn: () => fetch('/api/posts').then((r) => r.json()), }) export const Route = createFileRoute('/posts')({ // Ensure the data is in the cache before render loader: ({ context }) => context.queryClient.ensureQueryData(postsQuery), component: PostsPage, }) function PostsPage() { // Prefer suspense for best SSR + streaming behavior const { data } = useSuspenseQuery(postsQuery) return <div>{data.map((p: any) => p.title).join(', ')}</div> } // src/routes/posts.tsx import { queryOptions, useSuspenseQuery, useQuery } from '@tanstack/react-query' import { createFileRoute } from '@tanstack/react-router' const postsQuery = queryOptions({ queryKey: ['posts'], queryFn: () => fetch('/api/posts').then((r) => r.json()), }) export const Route = createFileRoute('/posts')({ // Ensure the data is in the cache before render loader: ({ context }) => context.queryClient.ensureQueryData(postsQuery), component: PostsPage, }) function PostsPage() { // Prefer suspense for best SSR + streaming behavior const { data } = useSuspenseQuery(postsQuery) return <div>{data.map((p: any) => p.title).join(', ')}</div> } You can also prefetch with fetchQuery or ensureQueryData in a loader without consuming the data in a component. If you return the promise directly from the loader, it will be awaited and thus block the SSR request until the query finishes. If you don't await the promise nor return it, the query will be started on the server and will be streamed to the client without blocking the SSR request.
import { createFileRoute } from '@tanstack/react-router' import { queryOptions, useQuery } from '@tanstack/react-query' const userQuery = (id: string) => queryOptions({ queryKey: ['user', id], queryFn: () => fetch(`/api/users/${id}`).then((r) => r.json()), }) export const Route = createFileRoute('/user/$id')({ loader: ({ params }) => { // do not await this nor return the promise, just kick off the query to stream it to the client context.queryClient.fetchQuery(userQuery(params.id)) }, }) import { createFileRoute } from '@tanstack/react-router' import { queryOptions, useQuery } from '@tanstack/react-query' const userQuery = (id: string) => queryOptions({ queryKey: ['user', id], queryFn: () => fetch(`/api/users/${id}`).then((r) => r.json()), }) export const Route = createFileRoute('/user/$id')({ loader: ({ params }) => { // do not await this nor return the promise, just kick off the query to stream it to the client context.queryClient.fetchQuery(userQuery(params.id)) }, }) If a query or mutation throws a redirect(...), the integration intercepts it on the client and performs a router navigation.
TanStack Start uses TanStack Router under the hood. The same setup applies, and the integration will stream query results during SSR automatically.
