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
New: Your Idea. Our Experts. We'll Build Your SaaS For You.
Our 'Done For You' service simplifies the process, taking your concept and delivering a production-ready MVP or complete SaaS solution.
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.