Guide for implementing SSR (Server-Side Rendering) with TanStack Query in Next.js App Router. Use this skill whenever the user asks about data fetching with TanStack React Query in Next.js server components, dehydrating state, hydration boundaries, prefetching queries, or organizing query providers for SSR. This is crucial to avoid hydration mismatches, memory leaks, and multiple refetches.
A guide to implementing SSR with TanStack Query in Next.js App Router using prefetchQuery and HydrationBoundary. This approach allows you to fetch data on the server, serialize it, and seamlessly hydrate it on the client, eliminating loading states and preventing refetches.
app/page.tsx, etc.).HydrationBoundary and dehydrate.staleTime and gcTime) effectively to prevent memory leaks and hydration mismatches.Set up a client-side provider with staleTime to prevent immediate refetches. By default, queries should have a staleTime that avoids repeating fetches right after rendering.
// components/providers/query-provider.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactNode, useState } from 'react';
export function QueryProvider({ children }: { children: ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
},
},
}),
);
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
Integrate the provider in your root layout so the entire app can access the query client context.
// app/layout.tsx
import { QueryProvider } from '@/components/providers/query-provider';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<QueryProvider>{children}</QueryProvider>
</body>
</html>
);
}
In a server component, initialize a temporary query client. Use prefetchQuery to fetch the data and then wrap the nested components in a HydrationBoundary with the dehydrated state.
CRITICAL: Set gcTime on the server's QueryClient to something small so you don't leak memory, but not zero.
// app/page.tsx
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from '@tanstack/react-query';
import { getServerData } from '@/rpc/api';
import { MyComponent } from '@/components/my-component';
export default async function HomePage() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// 2 seconds to prevent memory leaks on server, but allow time for dehydration
gcTime: 2 * 1000,
},
},
});
// Prefetch data
await queryClient.prefetchQuery({
queryKey: ['data'],
queryFn: getServerData,
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<MyComponent />
</HydrationBoundary>
);
}
Use the useQuery hook with the exact same key. The data will be instantly available and hydrated; it will not fetch again over the network until staleTime expires.
// components/my-component.tsx
'use client';
import { useQuery } from '@tanstack/react-query';
import { getServerData } from '@/rpc/api';
export function MyComponent() {
const { data } = useQuery({
queryKey: ['data'],
queryFn: getServerData,
});
return <div>{data?.message}</div>;
}
staleTime Settings: Always set a staleTime (e.g., 5-60 seconds or more depending on need) on the server/client properly to prevent hydration mismatches and redundant client fetches.gcTime: Set gcTime on the server to a short duration (e.g., 2 * 1000 ms) to prevent memory leaks. Avoid gcTime: 0 as it can cause hydration errors by garbage collecting data before rendering is complete.useQuery on the client.queryClient.clear() after dehydrate() to free memory immediately.