Client-side data fetching in Next.js with TanStack Query runs through useQuery, a hook that manages server state as a cache. Responses are cached by query key rather than re-fetched on every render.
It deduplicates identical requests and revalidates data when it becomes stale. The cache lives in the browser, so the same data does not need to be fetched again on every navigation.
npm i @tanstack/react-queryThe library needs a query client and a provider before any hook will work. The query client is a plain object that stores cached data, and the provider hands it to the component tree through React context.
// app/providers.tsx
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
let browserClient: QueryClient | undefined;
function getQueryClient() {
if (typeof window === "undefined") return new QueryClient();
return (browserClient ??= new QueryClient());
}
export default function Providers({ children }: { children: React.ReactNode }) {
return <QueryClientProvider client={getQueryClient()}>{children}</QueryClientProvider>;
}This component is a Client Component because the query client holds cache state, and it still runs on the server during the initial render. That is why the client is not created at module level: a single module-level client would be shared by every request the server handles, so one visitor could be served another visitor's cached data.
The helper gives each server render a fresh client and reuses one client in the browser. Wrap the tree with this provider once, high enough that every client component reads the same cache.
// app/profile/page.tsx
"use client";
import { useQuery } from "@tanstack/react-query";
export default function ProfilePage() {
const { data, error, isPending } = useQuery({
queryKey: ["user"],
queryFn: () => fetch("/api/user").then((res) => res.json()),
});
if (isPending) return <p>Loading</p>;
if (error) return <p>Failed to load</p>;
return <h1>Hello, {data.name}</h1>;
}The queryKey identifies this query in the cache, and queryFn does the actual fetch. Two components using the same key share one cached request and revalidate together.
The queryFn receives a context object rather than the raw key, holding queryKey and an AbortSignal, so one function can serve several keys and cancel a request that is no longer needed.
Query states
useQuery returns isPending while the first load runs, error when the request fails, and data once it succeeds. Handle all three so a slow or broken request shows something useful instead of a blank component. isPending is true only on the first load, while isFetching is true during background refetches too, so choose the one that matches the UI you want.
Query keys
The queryKey is the identity of a request. Use an array that describes the resource, such as ["posts"] or ["post", id]. Changing the key starts a new query, while the same key shares one cached result.
Revalidation
By default a query is considered stale the moment it loads, so TanStack Query refetches it in the background when the component mounts, the window refocuses, or the network reconnects. Set staleTime to relax that for slow-changing data.
Mutations
Mutations use useMutation, and invalidateQueries tells a query to refetch after a write. That keeps the cache in sync without manually updating every read.
When to reach for TanStack Query
Use it when client-side data needs caching, background refetching, or optimistic updates. It shines for server state that is shared across many components, where one cache and one set of background refetches beat many ad-hoc effects. For fetching on the server instead, see fetching without useEffect, and for seeding the cache with server data see hydrating client caches.
Rune AI
Key Insights
- TanStack Query caches client-side server state.
- Wrap the app in QueryClientProvider with a query client.
- Make a new client per server render, one per browser session.
- useQuery takes a queryKey and a queryFn.
- The hook returns data, error, and isPending.
Frequently Asked Questions
Do I need QueryClientProvider?
What is a query key?
Is TanStack Query client-side?
Conclusion
TanStack Query manages client-side server state through useQuery, caching each key's data and exposing loading, error, and success states. Set up the provider once, then query from any client component.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.