Fetch API Data in a Next.js Client Component with TanStack Query
Fetch an existing API endpoint from a Next.js App Router Client Component with TanStack Query and handle every query state.

When a Client Component needs to request API data, give the request a query function and let TanStack Query manage its result. The example below requests posts from an already-existing /api/posts endpoint. It expects a JSON array of objects with numeric id and string title fields; [] is a valid response. If you still need to create the endpoint, start with Build and Call an API Endpoint with Next.js App Router Route Handlers.
Next.js lists React Query as an option for fetching data in Client Components (Next.js data-fetching guide). This walkthrough manages a client query; it does not prefetch posts in a Server Component or transfer a server query cache to the client. Also, 'use client' is a component boundary, not a guarantee that the component only renders in the browser. Client Components can participate in the initial server-rendering pass.
Add a provider above the query
Install @tanstack/react-query as a project dependency before using the imports below. TanStack Query setup starts with a QueryClient and a QueryClientProvider (TanStack Query guide). Put the provider in app/providers.tsx. The state initializer retains one client across renders of this provider instance:
'use client'
import { useState, type ReactNode } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
export default function Providers({ children }: { children: ReactNode }) {
const [queryClient] = useState(() => new QueryClient())
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
Wrap the root layout’s children in that provider. The layout does not need its own 'use client' directive to render a Client Component provider:
// app/layout.tsx
import type { ReactNode } from 'react'
import Providers from './providers'
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
A component calling useQuery must render beneath QueryClientProvider. This placement makes the query component in the next step usable from a page inside the layout without turning that page into a Client Component just to provide context.
Give the posts request a query function
Create app/components/QueryPostsList.tsx. The query function calls /api/posts, checks the HTTP response before parsing JSON, and returns a Post[]. The response-shape check is intentional: a TypeScript return type alone does not inspect JSON received over HTTP. Throwing for a failed response or unexpected body lets the component show its error branch rather than treating bad data as an empty list.
'use client'
import { useQuery } from '@tanstack/react-query'
type Post = { id: number; title: string }
function isPostArray(value: unknown): value is Post[] {
return (
Array.isArray(value) &&
value.every(
(item) =>
typeof item === 'object' &&
item !== null &&
'id' in item &&
typeof item.id === 'number' &&
'title' in item &&
typeof item.title === 'string',
)
)
}
async function getPosts(): Promise<Post[]> {
const response = await fetch('/api/posts')
if (!response.ok) {
throw new Error(`Request failed (${response.status})`)
}
const body: unknown = await response.json()
if (!isPostArray(body)) {
throw new Error('The posts response has an unexpected shape')
}
return body
}
export default function QueryPostsList() {
const { data, error, isPending } = useQuery({
queryKey: ['posts'],
queryFn: getPosts,
})
if (isPending) return <p>Loading posts…</p>
if (error) return <p role="alert">Could not load posts: {error.message}</p>
if (data.length === 0) return <p>No posts yet.</p>
return (
<ul>
{data.map((post) => <li key={post.id}>{post.title}</li>)}
</ul>
)
}
Render <QueryPostsList /> from a page beneath the root layout. The stable ['posts'] key identifies this query, while getPosts supplies its data. With a successful non-empty response, the component can show “Loading posts…” while pending and then render the returned titles. A successful empty array shows “No posts yet.” An unsuccessful HTTP response, rejected request, invalid JSON, or body that fails the shape check leads to the error state. The checks come before data.length, so the component does not read it while data is unavailable.
Keep the client query at the API boundary
Use the existing API endpoint as queryFn does here, or an RPC layer such as tRPC. Do not substitute a Server Action for the query function. TanStack’s guidance warns that Server Actions called from the client run serially, which conflicts with query fetching and refetching and can leave queries pending (TanStack Query guide).
For one small, isolated client request, decide whether adding a query library and provider is worthwhile. It becomes more useful when the UI benefits from query-managed states or shared queries. Its cache and related behavior have TanStack Query’s own semantics, not Next.js server-fetch caching rules; this example does not configure a refetch policy. If you instead need to prefetch query data on the server and transfer it to a Client Component, that is the separate How to Prefetch TanStack Query Data in the Next.js App Router with HydrationBoundary walkthrough.




