Skip to content
State Management

TanStack Query — Component Library Guide

Powerful asynchronous state management for React. Automatic caching, background updates, stale-while-revalidate, and DevTools included.

★ Editor's Pick TypeScript MIT
5.6M weekly downloads
43.0k GitHub stars
~13kB bundle size
v5.68.0 latest version
data-fetchingcachingasyncserver-statedevtools

TL;DR: TanStack Query manages server state in React. useQuery() fetches and caches data; useMutation() handles writes with optimistic updates. Automatic background refetches, stale-while-revalidate caching, and DevTools included. ~13kB. Official docs: tanstack.com/query.

What is TanStack Query?

TanStack Query (formerly React Query) manages server state - data that lives on a server and needs to be fetched, cached, synchronized, and updated. It handles the lifecycle that every data-fetching implementation reinvents: loading states, error states, background refetches, cache invalidation, and optimistic updates.

The key insight: your component shouldn't manage fetching logic. useQuery subscribes to a cache entry; TanStack Query handles the rest.

When to use it

Use TanStack Query when your app fetches data from an API. It replaces:

  • useEffect + useState + manual loading/error flags
  • Redux/Zustand stores used purely as an API response cache
  • Custom data-fetching hooks that don't handle staleness

Skip it for client-only state (UI toggles, form state, wizard steps) - use Zustand or Jotai instead.

Key Features

  • Automatic caching - responses cached by query key; deduplicates parallel requests
  • Stale-while-revalidate - shows cached data immediately, refetches in background
  • Window focus refetching - refetches when the user returns to the tab
  • Mutations - useMutation for POST/PUT/DELETE with optimistic updates
  • Infinite queries - useInfiniteQuery for pagination and infinite scroll
  • DevTools - visual inspector for cache state, query status, and timing

Installation

npm install @tanstack/react-query
# DevTools (dev only):
npm install @tanstack/react-query-devtools

Basic Usage

// 1. Wrap your app
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

const queryClient = new QueryClient()

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MyApp />
    </QueryClientProvider>
  )
}

// 2. Fetch data in any component
import { useQuery } from '@tanstack/react-query'

function UserProfile({ userId }: { userId: string }) {
  const { data, isPending, isError } = useQuery({
    queryKey: ['users', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
  })

  if (isPending) return <Skeleton />
  if (isError) return <ErrorMessage />
  return <Profile user={data} />
}

Mutations with Cache Invalidation

import { useMutation, useQueryClient } from '@tanstack/react-query'

function DeleteButton({ postId }: { postId: string }) {
  const queryClient = useQueryClient()

  const deleteMutation = useMutation({
    mutationFn: (id: string) => fetch(`/api/posts/${id}`, { method: 'DELETE' }),
    onSuccess: () => {
      // Re-fetch the posts list after deletion
      queryClient.invalidateQueries({ queryKey: ['posts'] })
    },
  })

  return (
    <button
      onClick={() => deleteMutation.mutate(postId)}
      disabled={deleteMutation.isPending}
    >
      Delete
    </button>
  )
}

TypeScript Tips

Type your query functions for full inference:

interface Post { id: string; title: string; body: string }

const { data } = useQuery({
  queryKey: ['posts'],
  queryFn: (): Promise<Post[]> => fetch('/api/posts').then(r => r.json()),
})
// data is Post[] | undefined  -  automatically inferred

Common Gotchas

  • queryKey must be serializable - use primitive values or plain objects; avoid class instances or functions in the key
  • staleTime defaults to 0 - every mount triggers a background refetch. Set staleTime: 5 * 60 * 1000 globally for data that doesn't change often
  • Parallel queries - use useQueries for a dynamic list of queries rather than calling useQuery in a loop
  • Error boundaries - set throwOnError: true on the QueryClient to let errors bubble to React Error Boundaries
  • Zustand - use Zustand for client UI state alongside TanStack Query for server state
  • Recharts - pass useQuery data directly to Recharts chart components for live-updating dashboards with automatic background refetches
  • React 19 Guide - React 19's use() hook and Actions complement TanStack Query for server-first patterns
  • TypeScript Generics Guide - useQuery<TData, TError> and useMutation are fully generic; understand the patterns here

Frequently Asked Questions

What is the difference between TanStack Query and SWR?

Both handle server state caching but TanStack Query has a significantly richer feature set: built-in DevTools, mutation management with optimistic updates, infinite queries, query invalidation, and prefetching. SWR is simpler and smaller. For complex data requirements, TanStack Query is the better choice.

Does TanStack Query replace Zustand or Redux?

Only for server state. TanStack Query manages data that comes from an API - fetching, caching, and synchronising it. For client-only state (UI toggles, form state, user preferences), you still need Zustand, Jotai, or useState.

How do I prevent TanStack Query from refetching too often?

Set staleTime on the QueryClient to control how long data is considered fresh. The default is 0, which refetches on every mount. Setting staleTime: 5 * 60 * 1000 means data is reused for 5 minutes before a background refetch occurs.

How do I invalidate the cache after a mutation?

In the useMutation onSuccess callback, call queryClient.invalidateQueries with the relevant query key. This marks the cached data as stale and triggers a background refetch in any mounted component that subscribes to that query.

Can TanStack Query work with server-side rendering?

Yes. Use the HydrationBoundary and dehydrate utilities to prefetch queries on the server and rehydrate state on the client. Next.js App Router and React Server Components are fully supported.