Eliminate loading states with server-side prefetching for TanStack Query

8/27/2025

#next.js#tanstack#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 TanStack Start

Get started

Stay up to date

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