Eliminate loading states with server-side prefetching for TanStack Query
8/27/2025
Have you ever wanted to avoid a loading spinner on a page that fetches data when using Tanstack Query?
Fetching data on the client-side often results in a "flash of content" as the data arrives.
In a Next.js application, you can prefetch your queries on the server and deliver a fully-rendered page with zero loading states, while still keeping all the client-side benefits of Tanstack Query.
This is a powerful technique where you fetch your data on the server and then "hydrate" the client-side cache so your components can render instantly.
Here's how to implement it:
// app/page.tsx
import { QueryClient, HydrationBoundary, dehydrate } from '@tanstack/react-query';
// A simple function to fetch data
async function fetchTodos() {
const response = await fetch('[https://jsonplaceholder.typicode.com/todos](https://jsonplaceholder.typicode.com/todos)');
return response.json();
}
export default async function Page() {
// 1. Create a new QueryClient instance on every request
const queryClient = new QueryClient();
// 2. Prefetch the query on the server
// This fetches and caches the data before the component renders
await queryClient.prefetchQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
});
// 3. Dehydrate the state and pass it to the client
// The dehydrated state will be available to the client-side useQuery hook
const dehydratedState = dehydrate(queryClient);
return (
<HydrationBoundary state={dehydratedState}>
{/* Your client component will use useQuery here */}
<TodosList />
</HydrationBoundary>
);
}
// components/TodosList.tsx (Client Component)
// This file is a separate Client Component with 'use client' at the top
'use client';
import { useQuery } from '@tanstack/react-query';
function TodosList() {
// useQuery will find the data in the pre-fetched cache,
// so it will not show a loading state
const { data: todos, isLoading } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos, // You need to provide the queryFn again here
});
if (isLoading) {
return <div>Loading...</div>;
}
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
How it works:
QueryClientProvider
andHydrationBoundary
: You wrap your application in these providers to make the client-side cache available on the server and to serialize the data for the client.queryClient.prefetchQuery()
: This function fetches your data on the server-side, populating the cache.dehydrate()
: This serializes the populated cache into a plain JSON object.HydrationBoundary
: This component receives the serialized state and hydrates the client-side cache.useQuery()
on the client: When this hook runs, it finds the data already in the cache, so it renders immediately without a loading state.
This method is perfect for pages where you need to display dynamic data without a flicker, such as a product page or a user's dashboard.
For a more detailed explanation, you can check out the TanStack Query SSR documentation
More Dev Tips
Discover more tips and tricks to level up your development skills
Start your scalable and production-ready SaaS today
Save endless hours of development time and focus on what's important for your customers with our SaaS starter kits for Next.js, Nuxt 3 and SvelteKit
Get startedStay up to date
Sign up for our newsletter and we will keep you updated on everything going on with supastarter.