TanStack Query in Next.js
Next.js vs TanStack Query Caching
First, let's compare the caching mechanisms of Next.js and TanStack Query.
| Feature | Next.js Caching | TanStack Query Caching |
|---|---|---|
| Scope | Server-side and client-side | Primarily client-side |
| Main Purpose | Optimize page and API response performance | Efficiently manage server data and provide fast user experience |
| Caching Timing | At build time (SSG), request time (SSR) | At data fetching time |
| Caching Mechanism | - Static pages at build time - Server-rendered pages cached - API responses cached | - Query key-based caching - Automatic refetching & synchronization |
| Data Freshness | Re-generate static pages periodically (revalidate) | Automatic refetch (staleTime, cacheTime, refetchOnWindowFocus, etc.) |
| Data Mutation & Sync | Server-side mutations & synchronization | Mutations to update client cache & optimistic updates |
| Cache Control | HTTP cache headers for browser/CDN control | Client-side control (staleTime, cacheTime, etc.) |
| Persistence | Data persistence via server responses | Possible persistence to local/session storage (persistQueryClient) |
TanStack Query vs SWR
Both TanStack Query and SWR are React libraries that simplify data fetching, caching, and synchronization.
| Feature | TanStack Query | SWR |
|---|---|---|
| Data Fetching & Caching | Automatic caching, reuse, background updates | Automatic caching & revalidation for freshness |
| Server State Sync | Flexible strategies & detailed config | Automatic revalidation on changes |
| Background Updates | Highly configurable | Supported via focus revalidation, etc. |
| Pagination & Infinite Scroll | Easy hooks (useInfiniteQuery) | useSWRInfinite hook |
| Query Cancellation & Retries | Full support for cancellation & retry management | Retries supported, cancellation limited |
| Ease of Use | Rich features, detailed control, devtools | Simple, intuitive API, quick start |
If you don't need fine-grained control over data fetching, SWR's simplicity might be better.
For complex data management, debugging (devtools), or advanced caching strategies, TanStack Query is often the stronger choice.
Note: React Query has been rebranded as TanStack Query (part of the TanStack ecosystem).
react-query==tanstack-query.
Installing TanStack Query
# Core library
npm install @tanstack/react-query
# Devtools (optional, great for debugging)
npm install @tanstack/react-query-devtools
# ESLint plugin (if needed)
npm install -D @tanstack/eslint-plugin-query
The experimental @tanstack/react-query-next-experimental package is optional for advanced streaming/hydration scenarios but not required for most modern setups.
Choosing a Prefetching Strategy
There are two main approaches for prefetching in Next.js App Router:
-
initialData: Fetch data on the server and pass it as a prop to Client Components. Simple & fast setup, but can lead to prop drilling in deep trees. -
<HydrationBoundary>(formerly<Hydrate>in v4): Prefetch on server,dehydratethe cache state, andrehydrateon client via<HydrationBoundary>. More powerful for complex apps; avoids prop drilling and works seamlessly with Suspense/streaming.
Terminology
- Prop Drilling: Passing data manually through component props from parent to deep children.
- Hydration: Next.js sends initial HTML first (static or SSR). Then JavaScript "hydrates" interactive parts (attaches event listeners, makes React manage the DOM). Only Client Components (
'use client') get hydrated — Server Components do not.
'use client'means the component needs interactivity/browser APIs, not that it's rendered only on the client. All components render on the server first; Client Components then hydrate in the browser.
1. Setting Up QueryClientProvider
Wrap your app with QueryClientProvider (use a singleton on client, fresh on server).
// app/providers.tsx
'use client'
import { useState } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute - avoid immediate refetch
},
},
})
}
let browserQueryClient: QueryClient | undefined = undefined
function getQueryClient() {
if (typeof window === 'undefined') {
// Server: always new instance
return makeQueryClient()
} else {
// Client: singleton
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
}
}
export default function Providers({ children }: { children: React.ReactNode }) {
const queryClient = getQueryClient()
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
Then wrap in root layout:
// app/layout.tsx
import Providers from '@/providers'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
2. Using Queries in Client Components
// app/page.tsx (or any Client Component)
'use client'
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
import { Suspense } from 'react'
export default function Page() {
const { data, isLoading } = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('https://jsonplaceholder.typicode.com/todos').then(res => res.json()),
})
// Or with Suspense (v5+ preferred for streaming)
// const { data: suspenseData } = useSuspenseQuery({ queryKey: ['todos'], queryFn: ... })
return (
<div>
{isLoading ? 'Loading...' : (
<ul>
{data?.map((todo: any) => <li key={todo.id}>{todo.title}</li>)}
</ul>
)}
<Suspense fallback={<p>Loading suspense part...</p>}>
{/* <TodoList /> – can use useSuspenseQuery inside */}
</Suspense>
</div>
)
}
For parallel fetches with Suspense, use
useSuspenseQuery(v5+). In v4 it wasuseQueries.
Client-Side Streaming Hydration (Advanced/Optional)
For streaming data directly from useQuery / useSuspenseQuery in Client Components during SSR (experimental in some setups):
Install (if needed): npm i @tanstack/react-query-next-experimental
Wrap with ReactQueryStreamedHydration:
// app/providers.tsx (modified)
import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental'
// ...
return (
<QueryClientProvider client={queryClient}>
<ReactQueryStreamedHydration>
{children}
</ReactQueryStreamedHydration>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
Then in Client Component:
useQuery({
queryKey: ['todos'],
queryFn: ...,
suspense: true, // Enables streaming
})
Note: Streaming hydration can hurt SEO (content arrives progressively). For most apps in 2026, prefer server prefetching +
<HydrationBoundary>for SEO + performance. Use Server Components / Server Actions for initial data, and TanStack Query only for client-side interactivity (mutations, infinite scroll, optimistic UI, etc.).
If your app doesn't need complex client features (e.g., infinite scroll), Next.js native fetching (Server Components + fetch with caching) + Server Actions often suffices — no extra library needed.
Official docs (2026): https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr
Related Posts in Series
Collapse- 1. TanStack Query in Next.js