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-appInstall the TanStack packages:
1npm install @tanstack/react-query @tanstack/react-table @tanstack/react-routerAlso 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-devtoolsIn 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
Why TanStack Has Become the Default Choice for Modern React Apps
Once you use TanStack in a real project, it becomes clear why so many teams rely on it. These libraries don’t hide complexity they manage it properly. Instead of scattering data logic across components, TanStack centralizes server state and UI logic in a predictable way.
This is especially important in large Next.js applications where performance and consistency matter.
Server State vs Client State: A Mental Shift
A common mistake in React apps is treating server data like local state.
TanStack Query forces a healthier separation:
- Server state → fetched, cached, synchronized
- Client state → UI interactions, form input, modals
Once this boundary is clear, bugs reduce and components become easier to reason about.
Performance Gains You Actually Notice
In production apps, TanStack Query improves performance in ways users can feel:
- Instant data from cache when revisiting pages
- Fewer unnecessary network requests
- Automatic background refetching keeps data fresh
- Smooth pagination without loading flickers
These improvements increase user trust and reduce backend load.
Scaling TanStack in Large Codebases
As applications grow, teams often:
- Create shared query key factories
- Centralize fetch logic
- Standardize error handling
- Use query invalidation patterns consistently
This turns TanStack into part of the application architecture, not just a library.
📁 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

