storieasy-logo

Integrate TanStack Query with Next.js App Router (2025 Ultimate Guide)

dhruvesh borad

Info

24 May 2025

|

15 min to read

Integrating-TanStack-Query-in-Next.js

React Query

TanStack

Next.js

UseQuery

MutationFn

React Query Hook Next.js

React Query Custom Fetch

React Query Retry Config

Modern React applications demand performance, scalability, and clean data-fetching logic. That’s where TanStack shines — a powerful set of headless libraries (Query, Table, Router, and more) designed to work seamlessly with frameworks like Next.js. In this blog, we’ll dive deep into how to integrate TanStack (Query, Table, and Router) in a Next.js project. From initial setup to advanced usage, you’ll learn step-by-step with real-world examples.

📦 What is TanStack?

TanStack is a collection of high-quality libraries by Tanner Linsley for React and other JavaScript ecosystems:

  • @tanstack/react-query: For managing server-state and caching.
  • @tanstack/react-table: Headless table utilities for building flexible UI tables.
  • @tanstack/react-router: An advanced, type-safe router for React apps.
  • Other tools include virtual, form, etc.

📁 Project Setup (Basic)

Start with a clean Next.js app.

1npx create-next-app@latest tanstack-nextjs-app
2cd tanstack-nextjs-app

Install the TanStack packages:

1npm install @tanstack/react-query @tanstack/react-table @tanstack/react-router

Also install axios or fetch depending on how you plan to get data.

1. Setting Up TanStack Query in Next.js

🧠 Why React Query?

React Query handles fetching, caching, syncing, and updating server state in your app — with features like stale time, retry, refetch, pagination, etc.

🔧 Create a Query Client

/lib/queryClient.ts

1"use client";
2
3import { QueryClient } from "@tanstack/react-query";
4
5export const queryClient = new QueryClient({
6  defaultOptions: {
7    queries: {
8      retry: 1,
9    },
10    mutations: {
11      onError: (error) => {
12        console.error("Mutation Error:", error);
13        if (typeof window !== "undefined") {
14          alert("An error occurred while mutating data.");
15        }
16      },
17    },
18  },
19});

🔌 Integrate in layout.tsx

1import type { Metadata } from "next";
2import "./globals.css";
3import QueryProvider from "@/lib/QueryClientProvider";
4
5export const metadata: Metadata = {
6  title: "Create Next App",
7  description: "Generated by create next app",
8};
9
10export default function RootLayout({
11  children,
12}: Readonly<{
13  children: React.ReactNode;
14}>) {
15  return (
16    <html lang="en">
17      <body>
18        <QueryProvider>{children}</QueryProvider>
19      </body>
20    </html>
21  );
22}
23

📡 Fetching Data Example with React Query

/app/users/page.tsx

1"use client";
2import UserTable from "@/components/UserTable";
3import { useQuery } from "@tanstack/react-query";
4import axios from "axios";
5
6const fetchUsers = async () => {
7  const { data } = await axios.get(
8    "https://jsonplaceholder.typicode.com/users"
9  );
10  return data;
11};
12
13export default function UsersPage() {
14  const { data, isLoading, error } = useQuery({
15    queryKey: ["users"],
16    queryFn: fetchUsers,
17    refetchInterval: 30000, // Every 30 sec
18    staleTime: 1000 * 60 * 5, // 5 mins
19  });
20
21  if (isLoading) return <p>Loading...</p>;
22  if (error) return <p>Error occurred</p>;
23
24  return <UserTable data={data} />;
25}

⚙️ What’s happening?

  • queryKey: Unique key for caching (['users'])
  • queryFn: Function that fetches the data
  • Smart Caching: Results are stored and reused until stale

2. Using TanStack Table with Next.js

🧠 Why React Table?

Unlike UI frameworks, @tanstack/react-table provides headless logic for tables — letting you build custom UIs with full control over features like sorting, filtering, and pagination.

🛠️ Setup Table Component

/components/UserTable.tsx

1"use client";
2
3import { useMutation, useQueryClient } from "@tanstack/react-query";
4
5interface UserInput {
6  name: string;
7  email: string;
8}
9
10interface UserResponse {
11  id: string;
12  name: string;
13  email: string;
14  // Add other fields returned by the API if needed
15}
16
17const createUser = async (user: UserInput): Promise<UserResponse> => {
18  const res = await fetch("/api/users", {
19    method: "POST",
20    body: JSON.stringify(user),
21    headers: {
22      "Content-Type": "application/json",
23    },
24  });
25
26  if (!res.ok) {
27    const errorData = await res.json(); // If your backend returns a message
28    throw new Error(errorData.message || "Failed to create user");
29  }
30
31  return res.json();
32};
33
34export default function AddUser() {
35  const queryClient = useQueryClient();
36
37  const mutation = useMutation({
38    mutationFn: createUser,
39    onSuccess: () => {
40      queryClient.invalidateQueries({ queryKey: ["users"] });
41    },
42    onError: (error: Error) => {
43      alert(error.message);
44    },
45  });
46
47  const handleAddUser = () => {
48    mutation.mutate({ name: "Jane Doe", email: "jane@example.com" });
49  };
50
51  return <button onClick={handleAddUser}>Add User</button>;
52}

3. Advanced Features of React Query

🔁 Refetching & Stale Time

1useQuery({
2  queryKey: ['users'],
3  queryFn: fetchUsers,
4  staleTime: 1000 * 60 * 5, // 5 minutes
5  refetchOnWindowFocus: false,
6});
  • staleTime: How long data is considered fresh
  • refetchOnWindowFocus: Prevents auto-refresh when user switches tabs

📦 4. Pagination

1const fetchUsers = async (page: number) => {
2  const { data } = await axios.get(`/api/users?page=${page}`);
3  return data;
4};
5
6const [page, setPage] = useState(1);
7
8const { data } = useQuery({
9  queryKey: ['users', page],
10  queryFn: () => fetchUsers(page),
11});
  • The queryKey includes the page number so each page gets cached separately.

🔄 5. Mutations (POST/PUT/DELETE)

React Query is not just for GETs!

1"use client";
2
3import { useMutation, useQueryClient } from "@tanstack/react-query";
4
5interface UserInput {
6  name: string;
7  email: string;
8}
9
10interface UserResponse {
11  id: string;
12  name: string;
13  email: string;
14  // Add other fields returned by the API if needed
15}
16
17const createUser = async (user: UserInput): Promise<UserResponse> => {
18  const res = await fetch("/api/users", {
19    method: "POST",
20    body: JSON.stringify(user),
21    headers: {
22      "Content-Type": "application/json",
23    },
24  });
25
26  if (!res.ok) {
27    const errorData = await res.json(); // If your backend returns a message
28    throw new Error(errorData.message || "Failed to create user");
29  }
30
31  return res.json();
32};
33
34export default function AddUser() {
35  const queryClient = useQueryClient();
36
37  const mutation = useMutation({
38    mutationFn: createUser,
39    onSuccess: () => {
40      queryClient.invalidateQueries({ queryKey: ["users"] });
41    },
42    onError: (error: Error) => {
43      alert(error.message);
44    },
45  });
46
47  const handleAddUser = () => {
48    mutation.mutate({ name: "Jane Doe", email: "jane@example.com" });
49  };
50
51  return <button onClick={handleAddUser}>Add User</button>;
52}
  • mutationFn: Function that performs the mutation
  • onSuccess: Invalidate cache to refetch updated data

🛠 6. DevTools

Great for debugging React Query state!

1npm install @tanstack/react-query-devtools

In QueryClientProvider.tsx:

1import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
2
3<QueryClientProvider client={queryClient}>
4  <Component {...pageProps} />
5  <ReactQueryDevtools initialIsOpen={false} />
6</QueryClientProvider>

🌐 7. Server-Side Rendering (SSR) with Next.js(page route)

React Query supports SSR with hydration.

📥 Server-Side Data

pages/users.tsx

1import { dehydrate, Hydrate, QueryClient } from '@tanstack/react-query';
2
3export async function getServerSideProps() {
4  const queryClient = new QueryClient();
5  await queryClient.prefetchQuery({
6    queryKey: ['users'],
7    queryFn: fetchUsers,
8  });
9
10  return {
11    props: {
12      dehydratedState: dehydrate(queryClient),
13    },
14  };
15}
16
17export default function Page({ dehydratedState }) {
18  return (
19    <QueryClientProvider client={queryClient}>
20      <Hydrate state={dehydratedState}>
21        <UsersPage />
22      </Hydrate>
23    </QueryClientProvider>
24  );
25}
  • prefetchQuery runs server-side
  • Hydrate restores data on the client

📁 Best Practices

  • Use specific keys for better cache control: ['user', userId]
  • Avoid nesting query calls — use dependent queries instead
  • Use selectors to shape the returned data
  • Use enabled: false for conditional queries
1useQuery({
2  queryKey: ['user', userId],
3  queryFn: () => fetchUser(userId),
4  enabled: !!userId,
5});
PracticeBenefit
Use descriptive queryKeysBetter caching, easier debugging
Avoid useEffect for fetchingLet useQuery handle it
Invalidate cache after mutationKeeps UI in sync
Set staleTime smartlyAvoids unnecessary refetches
Use enabled: false for conditional queriesPrevents errors on null IDs

📚 Summary

FeatureWhat it DoesBenefit
useQueryFetch and cache server dataAutomatic caching and updates
useMutationHandle POST/PUT/DELETEClean, async mutation workflows
QueryClientGlobal configuration and cachingCentralized query management
HydrateServer-side hydration for SSRFast page loads with data prefetching
DevtoolsVisual debuggingInspect query cache and states

🧩 GitHub Example Repository

Want to see all of this in action?

🔗 Check out the full working example on GitHub:
👉 github.com/dhruveshborad/tanstack-nextjs-app

This repository includes:

  • ✅ Full integration with TanStack Query
  • ✅ App Router-based Next.js project
  • ✅ Error handling and devtools setup
  • ✅ Example useQuery and useMutation usage
  • ✅ Modular structure with QueryProvider and queryClient

Newsletter

Subscribe to our newsletter and get our latest updates.

Share with your friends: