| name | tanstack-query-guide |
| description | Guide for using TanStack Query (React Query) for server state management in React applications. Use when implementing data fetching, caching, mutations, optimistic updates, or managing server state. Apply when the user asks about TanStack Query, React Query, useQuery, useMutation, cache management, or data synchronization. |
| keywords | tanstack-query, react-query, useQuery, useMutation, data-fetching, cache, optimistic-update, query-client |
TanStack Query Guide
Overview
TanStack Query (formerly React Query) is a powerful data synchronization library for managing server state, providing automatic caching, background updates, and optimistic UI updates.
Setup
QueryClient Configuration
import { QueryClient } from '@tanstack/react-query'
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
gcTime: 5 * 60 * 1000,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
retry: 3,
},
},
})
Provider Setup
import { QueryClientProvider } from '@tanstack/react-query'
import { queryClient } from '@/lib/query-client'
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* Your app */}
</QueryClientProvider>
)
}
useQuery Hook
Basic Usage
import { useQuery } from '@tanstack/react-query'
function TodoList() {
const { data, isLoading, error, isFetching } = useQuery({
queryKey: ['todos'],
queryFn: async () => {
const response = await fetch('/api/todos')
if (!response.ok) throw new Error('Failed to fetch')
return response.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>
)
}
Query with Parameters
function TodoList({ filter }: { filter: string }) {
const { data, status } = useQuery({
queryKey: ['todos', { filter }],
queryFn: async () => {
const response = await fetch(`/api/todos?filter=${filter}`)
return response.json()
},
})
}
Conditional Query
function UserProfile({ userId }: { userId?: string }) {
const { data: user } = useQuery({
queryKey: ['users', userId],
queryFn: () => fetchUser(userId!),
enabled: !!userId,
})
}
Stale Time Configuration
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
})
useMutation Hook
Basic Mutation
import { useMutation, useQueryClient } from '@tanstack/react-query'
function AddTodoForm() {
const queryClient = useQueryClient()
const { mutate, isPending, isError, error } = useMutation({
mutationFn: async (newTodo: { title: string }) => {
const response = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo),
})
if (!response.ok) throw new Error('Failed to add todo')
return response.json()
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
const title = formData.get('title') as string
mutate({ title })
}
return (
<form onSubmit={handleSubmit}>
<input name="title" placeholder="New todo" />
<button type="submit" disabled={isPending}>
{isPending ? 'Adding...' : 'Add'}
</button>
{isError && <p>Error: {error.message}</p>}
</form>
)
}
Update Mutation
const updateTodoMutation = useMutation({
mutationFn: async ({ id, data }: { id: string; data: Partial<Todo> }) => {
const response = await fetch(`/api/todos/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
return response.json()
},
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: ['todos', variables.id] })
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
updateTodoMutation.mutate({ id: '123', data: { completed: true } })
Delete Mutation
const deleteTodoMutation = useMutation({
mutationFn: async (id: string) => {
await fetch(`/api/todos/${id}`, { method: 'DELETE' })
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
Optimistic Updates
Basic Optimistic Update
const updateTodoMutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] })
const previousTodos = queryClient.getQueryData(['todos'])
queryClient.setQueryData(['todos'], (old: Todo[]) =>
old.map(todo =>
todo.id === newTodo.id ? { ...todo, ...newTodo } : todo
)
)
return { previousTodos }
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context?.previousTodos)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
Cache Management
Invalidate Queries
queryClient.invalidateQueries({ queryKey: ['todos'] })
queryClient.invalidateQueries({ queryKey: ['todos', todoId] })
queryClient.invalidateQueries({
queryKey: ['todos', { filter: 'active' }],
exact: true
})
Prefetch Queries
const handleMouseEnter = () => {
queryClient.prefetchQuery({
queryKey: ['todos', todoId],
queryFn: () => fetchTodo(todoId),
})
}
return (
<Link to={`/todos/${todoId}`} onMouseEnter={handleMouseEnter}>
View Todo
</Link>
)
Manual Cache Update
queryClient.setQueryData(['todos', todoId], newTodo)
const todos = queryClient.getQueryData(['todos'])
queryClient.removeQueries({ queryKey: ['todos', todoId] })
Query Keys
Structure
['users']
['users', userId]
['users', userId, 'posts']
['users', userId, 'posts', { page: 1 }]
['user-123']
['user-123-posts']
Best Practices
['todos', { status: 'active', page: 1 }]
['users', userId, 'posts', postId]
['users', postId, 'posts', userId]
Status Management
Query Status
const { status, fetchStatus, data, error } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
if (status === 'pending') return <div>Loading...</div>
if (status === 'error') return <div>Error: {error.message}</div>
if (status === 'success') return <div>{data}</div>
Helpful Booleans
const {
isLoading,
isError,
isSuccess,
isFetching,
isRefetching,
isPending,
} = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
Refetch Behavior
Auto Refetch Options
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
refetchOnMount: true,
refetchInterval: 30000,
})
Manual Refetch
const { data, refetch } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
<button onClick={() => refetch()}>Refresh</button>
Error Handling
Query Error Handling
const { data, error, isError } = useQuery({
queryKey: ['todos'],
queryFn: async () => {
const response = await fetch('/api/todos')
if (!response.ok) {
throw new Error('Failed to fetch todos')
}
return response.json()
},
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
})
if (isError) {
return <div>Error: {error.message}</div>
}
Mutation Error Handling
const mutation = useMutation({
mutationFn: addTodo,
onError: (error, variables, context) => {
console.error('Failed to add todo:', error)
},
retry: 2,
})
Best Practices
- Use query keys hierarchically - Structure from general to specific
- Include all dependencies in query key - Ensure proper cache invalidation
- Set appropriate staleTime - Balance freshness vs network requests
- Use optimistic updates for better UX
- Invalidate related queries after mutations
- Prefetch data on user intent (hover, etc.)
- Handle loading and error states properly
- Use enabled option for conditional queries
For detailed patterns, see: