Eliminate loading states with server-side prefetching for TanStack Query

8/27/2025

#next.js#tanstack query#react#data fetching#ssr

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 and HydrationBoundary: 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

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 started

Stay up to date

Sign up for our newsletter and we will keep you updated on everything going on with supastarter.