This episode covers server-side rendering integration: dehydrate and hydrate with HydrationBoundary for the Next.js App Router, prefetching in server components, as well as integration with Remix loaders and framework-agnostic patterns using the query core.

In SSR (server-side rendering), the page is rendered on the server and then sent as HTML. The problem: if the data is only fetched on the client, the initial HTML is empty and the user sees a loading state. The TanStack Query solution: dehydrate the cache on the server, send its serialization along with the HTML, then hydrate on the client so the page is immediately full without refetching.
Episode 14 covers the hydration pattern for the Next.js App Router and Remix, plus how to use the query core outside a framework.
Two key functions: dehydrate freezes the cache into a serializable state, and hydrate restores it on the client side. Both are connected by HydrationBoundary:
server: prefetch → dehydrate → send serialized state
client: hydrate state → instant render from the cacheThe server: prefetch → dehydrate diagram shows the server steps, and client: hydrate state the client steps. HydrationBoundary is the bridge between the two.
In the App Router, server components can prefetch into a cache created on the server. TanStack Query provides the getQueryClient helper pattern to wrap one instance per request:
import { QueryClient, isServer } from "@tanstack/react-query"
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
})
}
let browserQueryClient = undefined
export function getQueryClient() {
if (isServer) {
return makeQueryClient()
}
if (!browserQueryClient) {
browserQueryClient = makeQueryClient()
}
return browserQueryClient
}getQueryClient returns a new instance on the server (per request) and the same instance in the browser (per session). isServer distinguishes the runtime context. This prevents cache leakage between requests on the server and multiple caches on the client.
In a server component, prefetch and then wrap with HydrationBoundary, which accepts the dehydrated cache:
import { dehydrate, HydrationBoundary } from "@tanstack/react-query"
import { getQueryClient } from "./get-query-client"
export default async function TodosPage() {
const queryClient = getQueryClient()
await queryClient.prefetchQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
})
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<Todos />
</HydrationBoundary>
)
}queryClient.prefetchQuery fills the cache on the server, then dehydrate(queryClient) turns the cache into a serializable state sent along with the HTML. HydrationBoundary state={dehydrate(queryClient)} restores the cache on the client — the Todos component using useQuery immediately finds the data and doesn't refetch.
In Remix, data is fetched in the server loader and passed to the component. To combine it with TanStack Query, seed the cache with the loader data via initialData on the client:
export async function loader() {
const todos = await fetchTodos()
return { todos }
}
export default function Todos({ loaderData }) {
const { data } = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
initialData: loaderData.todos,
staleTime: 60 * 1000,
})
return <TodoList todos={data} />
}initialData: loaderData.todos fills the cache from data already fetched by the loader. useQuery displays that data instantly and refreshes it in the background after staleTime expires. This approach is the simplest way to combine Remix and TanStack Query without a hydration boundary.
Because the core logic lives in @tanstack/query-core, all framework adapters use the same concepts — query keys, cache, observers. The knowledge you learn in this series applies to Vue Query, Svelte Query, and Solid Query without changing concepts, only the hook syntax differs. Episode 19 will cover this adapter ecosystem thoroughly.
Warning
In Next.js, never create a new QueryClient inside every component render. Use the getQueryClient pattern so the server creates one instance per request and the browser uses one instance for the entire session — otherwise the cache will be reset over and over.
Episode 14 bridged TanStack Query with SSR: server prefetch, dehydrate, hydrate via HydrationBoundary, the getQueryClient pattern for the Next.js App Router, and seeding the cache from a Remix loader. You also saw that the core concepts are framework-agnostic.
Key takeaways:
dehydrate freezes the cache; hydrate restores it on the client.HydrationBoundary connects the server and client caches.getQueryClient distinguishes server and browser instances.initialData from a loader.In the next episode, episode 15, we will discuss security and best practice — handling 401/403 with refresh tokens, avoiding excessive sensitive data in the cache, sanitizing errors, and best practices for query keys and separating server state versus client state.