DEV Community

Cover image for TanStack Query 101: Stop Struggling with Data Fetching in Next.js
Ashik Ahmed
Ashik Ahmed

Posted on

TanStack Query 101: Stop Struggling with Data Fetching in Next.js

In modern web applications, managing server state (data from APIs) is often more complex than it should be. Handling loading states, caching, refetching, and errors manually quickly becomes messy. This is where TanStack Query (formerly React Query) shines. When combined with Next.js, it creates a powerful stack for building fast, scalable, and maintainable applications.

In this guide, you’ll learn:

  1. What TanStack Query is
  2. Why it pairs perfectly with Next.js
  3. How to set it up
  4. How to fetch, cache, and prefetch data
  5. Real-world patterns you’ll actually use

What is TanStack Query?

TanStack Query is a powerful asynchronous state management library designed specifically for handling server state.
Unlike traditional state management tools, it focuses on:

  • Fetching
  • Caching
  • Synchronizing
  • Updating server data efficiently

✨ Key Features

  • Data Fetching → Simplifies API calls
  • Caching → Avoid unnecessary requests
  • Background Refetching → Keeps data fresh
  • Error Handling & Retries → Built-in resilience
  • DevTools → Debug queries visually

In short:
It removes the need for messy useEffect + useState logic.

Why Use TanStack Query with Next.js?

Next.js supports:

  • SSR (Server-Side Rendering)
  • SSG (Static Site Generation)
  • CSR (Client-Side Rendering)

TanStack Query complements this perfectly.

** Benefits**

  • No more manual data fetching logic
  • Automatic caching = better performance
  • Works across SSR, SSG, CSR seamlessly
  • Cleaner and more maintainable code

🛠️ Getting Started

  1. Create a Next.js App
npx create-next-app@latest my-tanstack-query-app
cd my-tanstack-query-app
Enter fullscreen mode Exit fullscreen mode
  1. Install Dependencies
npm install @tanstack/react-query @tanstack/react-query-devtools
Enter fullscreen mode Exit fullscreen mode
  1. Create a Query Provider

TanStack Query requires a QueryClient to manage caching and queries.

📁 providers.js

`"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { useState } from "react";

export default function Providers({ children }) {
  // Prevent recreating client on every render
  const [queryClient] = useState(() => new QueryClient());

  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}`
Enter fullscreen mode Exit fullscreen mode
  1. Wrap Your Application 📁 app/layout.js
`import Providers from "../providers";

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}`
Enter fullscreen mode Exit fullscreen mode

🔥 Fetching Data with useQuery

Now let’s fetch real data.

📁 app/page.js

`"use client";

import { useQuery } from "@tanstack/react-query";

async function fetchPosts() {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts");

  if (!res.ok) {
    throw new Error("Failed to fetch posts");
  }

  return res.json();
}

export default function Home() {
  const { data, isLoading, isError, error } = useQuery({
    queryKey: ["posts"],
    queryFn: fetchPosts,
  });

  if (isLoading) return <p>Loading posts...</p>;
  if (isError) return <p>Error: {error.message}</p>;

  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {data.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}`
Enter fullscreen mode Exit fullscreen mode

🧩

Understanding Core Concepts

🔑 Query Keys

Unique identifiers for caching.

["posts"]          // all posts
["posts", 1]       // specific post
Enter fullscreen mode Exit fullscreen mode

Changing the key = different cache entry.

⚙️ Query Function

An async function that returns data:

`const fetchPosts = async () => { ... }`
Enter fullscreen mode Exit fullscreen mode

📊 Query States

  • isLoading → First fetch
  • isError → If request fails
  • error → Error details
  • data → Final result

⚡ Server-Side Prefetching (SSR)

One of the most powerful features when using Next.js.
You can fetch data on the server and hydrate it on the client.

📁 app/page.js (Server Component)

`import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from "@tanstack/react-query";
import Posts from "../components/Posts";

async function fetchPosts() {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts");

  if (!res.ok) {
    throw new Error("Failed to fetch posts");
  }

  return res.json();
}

export default async function Home() {
  const queryClient = new QueryClient();

  await queryClient.prefetchQuery({
    queryKey: ["posts"],
    queryFn: fetchPosts,
  });

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <Posts />
    </HydrationBoundary>
  );
}`
Enter fullscreen mode Exit fullscreen mode

📁 components/Posts.js

`"use client";

import { useQuery } from "@tanstack/react-query";

export default function Posts() {
  const { data } = useQuery({
    queryKey: ["posts"],
  });

  return (
    <ul>
      {data.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}`
Enter fullscreen mode Exit fullscreen mode

** Result:**

  • No loading state on first render
  • Faster performance
  • Better SEO

🛠️ DevTools

The DevTools help you:

  • Inspect queries
  • See cache behavior
  • Debug easily

Already added in providers.js:

`<ReactQueryDevtools initialIsOpen={false} />`
Enter fullscreen mode Exit fullscreen mode

🔁 Common Real-World Patterns

📄 Pagination

`const { data } = useQuery({
  queryKey: ["posts", page],
  queryFn: () => fetchPosts(page),
});`
Enter fullscreen mode Exit fullscreen mode

♾️ Infinite Scrolling

`import { useInfiniteQuery } from "@tanstack/react-query";

const { data, fetchNextPage } = useInfiniteQuery({
  queryKey: ["posts"],
  queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam),
  getNextPageParam: (lastPage) => lastPage.nextPage,
});`
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

TanStack Query is a true game-changer when it comes to managing server state in modern applications. Instead of manually handling loading states, caching logic, and refetching strategies, it provides a clean and declarative way to work with asynchronous data.

With features like:

  • Smart caching that reduces unnecessary network requests
  • Background updates to keep your UI always in sync
  • Built-in support for SSR and hydration with Next.js

…it significantly simplifies your codebase while also improving performance and user experience.

As your application grows, these benefits become even more noticeable. What starts as a small improvement quickly turns into a major advantage in terms of scalability, maintainability, and developer productivity.

What’s Next?

Now that you’ve learned the fundamentals, the next step is to explore more advanced and real-world features of TanStack Query:

  • Handling mutations (POST, PUT, DELETE)
  • Implementing optimistic UI updates for better user experience
  • Managing cache with query invalidation strategies
  • Structuring queries in larger applications

These concepts will help you move from basic usage to building production-ready applications.

If you're learning in public like this, you're already ahead of many developers. Consistency is the key — keep building, keep sharing, and keep improving.

More in-depth topics are coming in the next part. Keep Learning, Keep Coding, and Never Stop Dreaming

Top comments (0)