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

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});
Practice | Benefit |
---|---|
Use descriptive queryKeys | Better caching, easier debugging |
Avoid useEffect for fetching | Let useQuery handle it |
Invalidate cache after mutation | Keeps UI in sync |
Set staleTime smartly | Avoids unnecessary refetches |
Use enabled: false for conditional queries | Prevents errors on null IDs |
📚 Summary
Feature | What it Does | Benefit |
---|---|---|
useQuery | Fetch and cache server data | Automatic caching and updates |
useMutation | Handle POST/PUT/DELETE | Clean, async mutation workflows |
QueryClient | Global configuration and caching | Centralized query management |
Hydrate | Server-side hydration for SSR | Fast page loads with data prefetching |
Devtools | Visual debugging | Inspect 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