TanStack Query for React provides powerful hooks for fetching, caching, and updating asynchronous data in your React applications.
Installation
npm install @tanstack/react-query
# or
pnpm add @tanstack/react-query
# or
yarn add @tanstack/react-query
Setup
Wrap your application with QueryClientProvider:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
)
}
Core Hooks
useQuery
Fetch and cache data with the useQuery hook:
import { useQuery } from '@tanstack/react-query'
function Todos() {
const { data, isLoading, error } = useQuery({
queryKey: ['todos'],
queryFn: async () => {
const res = await fetch('/api/todos')
return res.json()
},
})
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
useMutation
Perform side effects with the useMutation hook:
import { useMutation, useQueryClient } from '@tanstack/react-query'
function AddTodo() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: async (newTodo) => {
const res = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
})
return res.json()
},
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
return (
<button onClick={() => mutation.mutate({ title: 'New Todo' })}>
Add Todo
</button>
)
}
useInfiniteQuery
Implement infinite scrolling or pagination:
import { useInfiniteQuery } from '@tanstack/react-query'
function Posts() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: async ({ pageParam = 0 }) => {
const res = await fetch(`/api/posts?page=${pageParam}`)
return res.json()
},
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
initialPageParam: 0,
})
return (
<div>
{data?.pages.map((page) => (
page.posts.map((post) => <Post key={post.id} post={post} />)
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
</div>
)
}
Suspense Support
useSuspenseQuery
React Query has first-class support for React Suspense:
import { Suspense } from 'react'
import { useSuspenseQuery } from '@tanstack/react-query'
function TodoList() {
// This will suspend while loading
const { data } = useSuspenseQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<TodoList />
</Suspense>
)
}
useSuspenseQuery automatically sets enabled: true and suspense: true. The query will never return isLoading - it will suspend instead.
useSuspenseInfiniteQuery
Combine Suspense with infinite queries:
import { useSuspenseInfiniteQuery } from '@tanstack/react-query'
function InfinitePosts() {
const { data, fetchNextPage, hasNextPage } = useSuspenseInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 0 }) => fetchPosts(pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: 0,
})
// No loading state needed - component suspends automatically
return <PostList data={data} onLoadMore={fetchNextPage} />
}
Advanced Patterns
useQueries
Execute multiple queries in parallel:
import { useQueries } from '@tanstack/react-query'
function UserData({ userIds }) {
const results = useQueries({
queries: userIds.map((id) => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
})),
})
// results is an array of query results
const isLoading = results.some((result) => result.isLoading)
return <div>{/* render results */}</div>
}
Query Options Factory
Create reusable query configurations:
import { queryOptions, useQuery } from '@tanstack/react-query'
const todoQueries = {
all: () => queryOptions({
queryKey: ['todos'],
queryFn: fetchTodos,
}),
detail: (id: number) => queryOptions({
queryKey: ['todos', id],
queryFn: () => fetchTodo(id),
}),
}
function TodoDetail({ id }) {
const { data } = useQuery(todoQueries.detail(id))
return <div>{data.title}</div>
}
Error Boundaries
Handle errors with React Error Boundaries:
import { QueryErrorResetBoundary } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'
function App() {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={({ resetErrorBoundary }) => (
<div>
There was an error!
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)}
>
<YourApp />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
)
}
Hydration
For SSR frameworks like Next.js:
import { HydrationBoundary, dehydrate } from '@tanstack/react-query'
// Server-side
export async function getServerSideProps() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
return {
props: {
dehydratedState: dehydrate(queryClient),
},
}
}
// Client-side
function MyApp({ dehydratedState }) {
return (
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={dehydratedState}>
<YourApp />
</HydrationBoundary>
</QueryClientProvider>
)
}
TypeScript
Full TypeScript support with type inference:
interface Todo {
id: number
title: string
completed: boolean
}
function Todos() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: async (): Promise<Todo[]> => {
const res = await fetch('/api/todos')
return res.json()
},
})
// data is typed as Todo[] | undefined
return <div>{data?.length} todos</div>
}
React-Specific Features
Automatic Request Cancellation
React Query automatically cancels outgoing requests when components unmount:
function SearchResults({ query }) {
const { data } = useQuery({
queryKey: ['search', query],
queryFn: async ({ signal }) => {
// Pass the AbortSignal to fetch
const res = await fetch(`/api/search?q=${query}`, { signal })
return res.json()
},
})
return <div>{/* results */}</div>
}
The signal parameter in queryFn is automatically provided and will abort the request if the component unmounts or the query is cancelled.
Window Focus Refetching
Queries automatically refetch when the window regains focus:
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
refetchOnWindowFocus: true, // default
})
useMutationState
Access the state of all mutations:
import { useMutationState } from '@tanstack/react-query'
function GlobalLoadingIndicator() {
const pendingMutations = useMutationState({
filters: { status: 'pending' },
})
if (pendingMutations.length === 0) return null
return <div>Saving {pendingMutations.length} changes...</div>
}
Structural Sharing
React Query automatically uses structural sharing to minimize re-renders:
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
// Structural sharing is enabled by default
// Only changed objects trigger re-renders
})
Transform data without causing unnecessary re-renders:
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (data) => data.filter((todo) => !todo.completed),
// Only re-renders if the filtered result changes
})