React Query (TanStack Query) — Client & Server Usage Guide
Overview
React Query (TanStack Query) is a powerful library for managing, caching, and synchronizing server state in your React applications. It abstracts away the complexity of data fetching, state management, and synchronization, making it ideal for working with APIs and asynchronous data.
Client-Side Usage
To use React Query on the client side, you must wrap your components with the QueryClientProvider, making the query client available throughout your app’s component tree.
1. Setup the Query Client
Define the QueryClient and use the QueryClientProvider at the root of your application. The defaultOptions are crucial for defining global behavior.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false, // disable refetch when user switches back to tab
retry: false, // disable automatic retries on failure
staleTime: 1000 * 60 * 5, // default data stays fresh for 5 minutes
}
}
})
export function AppProvider({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}Note: The
defaultOptionsallow you to define default behaviors for all queries and mutations, ensuring consistency.
2. Using useQuery
The useQuery hook is the core tool for fetching, caching, and managing data retrieval.
import { useQuery } from '@tanstack/react-query'
const { data, isFetching, error, refetch } = useQuery({
queryKey: ['users'], // unique cache key for this query
queryFn: async () => {
const res = await fetch('/api/users')
if (!res.ok) throw new Error('Failed to fetch users')
return res.json()
},
select: (data) => data.users, // optional: transform data before returning
})| Common Returned Values | Description |
|---|---|
data | The resolved data from the query function. |
isFetching | Boolean indicating if the query is currently fetching (includes background refetches). |
isLoading | Boolean indicating if the query is fetching for the first time. |
error | Any thrown error from the query function. |
refetch() | Function to manually trigger a refetch of the query. |
3. Using useMutation
useMutation is used for any side effects that change server state (POST, PUT, DELETE, etc.).
import { useMutation, useQueryClient } from '@tanstack/react-query'
// Get the client instance to interact with the cache
const queryClient = useQueryClient()
const { mutate, isPending, error } = useMutation({
mutationFn: async (newUser) => {
// API call to create a new user
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newUser),
})
if (!res.ok) throw new Error('Failed to add user')
return res.json()
},
onSuccess: () => {
// Invalidate the cache to trigger a refetch of the 'users' query
queryClient.invalidateQueries({ queryKey: ['users'] })
},
})
// To trigger the mutation:
// mutate({ name: 'Tareq', email: 'tareq@example.com' })4. Other Query Types
React Query provides specialized hooks for advanced use cases:
useInfiniteQuery: Manages and fetches data for paginated/infinite scrolling lists.useQueries: Allows you to run multipleuseQueryinstances in parallel, simplifying state tracking.useSuspenseQuery: Integrates tightly with React Suspense for cleaner data loading states.
Server-Side Usage (SSR/SSG)
On the server side (e.g., in Next.js App Router or similar frameworks), you can prefetch queries during Server-Side Rendering (SSR) to ensure the data is immediately available to the client upon page load, minimizing loading spinners and improving UX. This process involves hydration.
1. Setup the Query Client (Server Side)
In an SSR environment, you need a mechanism to create a new QueryClient for every request on the server, while using a single persistent client on the browser.
import {
isServer,
QueryClient,
defaultShouldDehydrateQuery
} from '@tanstack/react-query'
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: false,
staleTime: 60 * 1000 // 1 minute stale time
},
dehydrate: {
// Includes pending queries in dehydration for suspense compatibility
shouldDehydrateQuery: query =>
defaultShouldDehydrateQuery(query) ||
query.state.status === 'pending',
// Next.js handles error redaction; we can disable it here
shouldRedactErrors: () => false
}
}
})
}
let browserQueryClient: QueryClient | undefined = undefined
// This function ensures a new client on the server, and a singleton on the client
export function getQueryClient() {
if (isServer) {
// Server: always make a new query client per request
return makeQueryClient()
} else {
// Browser: create a singleton client if one doesn't exist
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
}
}2. Prefetch Queries and Hydrate
Use prefetchQuery to fetch data on the server and HydrationBoundary to pass that data to the client.
And in the client component use useSuspenseQuery to prevent the query from refetching (userSuspenseQuery will take the result cashed through HydrationBoundary)
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import { getQueryClient } from './getQueryClient'
// This component runs on the server
export default async function Page() {
const queryClient = getQueryClient()
// 1. Prefetch the data on the server
await queryClient.prefetchQuery({
queryKey: ['users'],
queryFn: async () => {
const res = await fetch('https://api.example.com/users')
return res.json()
},
})
// 2. Dehydrate the query client state
const dehydratedState = dehydrate(queryClient)
return (
// 3. Hydrate the client with the server-fetched data
<HydrationBoundary state={dehydratedState}>
<ClientComponent /> {/* ClientComponent can now use useSuspenseQuery(['users']) */}
</HydrationBoundary>
)
}The
HydrationBoundarymakes the server-fetched data available to your client-side React Query hooks without triggering an extra network request on the client.
Recommended Docs
For the most up-to-date and detailed information, please refer to the official documentation:
Summary of Query Hooks
| Hook / Method | Purpose | Typical Use |
|---|---|---|
useQuery | Fetch & cache data. | Get a list of items or a single detail record from an API. |
useMutation | Modify data on the server. | POST, PUT, or DELETE requests. |
useInfiniteQuery | Fetch paginated/chunked data. | Implementing infinite scroll interfaces. |
useQueries | Run multiple queries concurrently. | Parallel fetching of independent data sets. |
prefetchQuery | Preload data on the server or client. | Server-side Rendering (SSR) data preloading. |