Client-Side Data Fetching in Next.js with TanStack Query

TanStack Query manages server state with a client cache. Learn to set up the provider and use useQuery in a Client Component.

7 min read

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.

bashbash
npm i @tanstack/react-query

The 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.tsxApp.tsx
// 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.tsxApp.tsx
// 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

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.
RunePowered by Rune AI

Frequently Asked Questions

Do I need QueryClientProvider?

Yes. It provides the query client that stores cached data, and every component using queries must be inside it. Create a fresh client per server render and reuse a single client in the browser.

What is a query key?

The identity of a query. TanStack Query caches each key's data separately and refetches when the key changes.

Is TanStack Query client-side?

Yes. Queries run in Client Components, and the cache lives in the browser.

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.