Queries are the foundation of TanStack Query. They represent a declarative dependency on an asynchronous source of data that is tied to a unique key.
Basic Query
To subscribe to a query in your components, use the useQuery hook with at least:
- A unique key for the query
- A function that returns a promise
import { useQuery } from '@tanstack/react-query'
function App() {
const { data, error, status } = useQuery({
queryKey: ['todos'],
queryFn: async () => {
const response = await fetch('https://api.example.com/todos')
return response.json()
}
})
if (status === 'pending') return <div>Loading...</div>
if (status === 'error') return <div>Error: {error.message}</div>
return (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
Query Results
The query result contains all information about the query state:
const {
// Data states
data, // The last successfully resolved data
error, // The error object if the query failed
status, // 'pending' | 'error' | 'success'
// Fetch states
fetchStatus, // 'fetching' | 'paused' | 'idle'
isFetching, // Is the query currently fetching?
isPending, // Is the query in pending state?
isLoading, // Is it both pending AND fetching?
// Derived states
isSuccess, // Did the query succeed?
isError, // Did the query fail?
isStale, // Is the data stale?
isPlaceholderData, // Is the data from placeholder?
// Actions
refetch, // Function to manually refetch
// Metadata
dataUpdatedAt, // Timestamp of last successful fetch
errorUpdatedAt, // Timestamp of last error
failureCount, // Number of consecutive failures
failureReason, // The error from the last failed attempt
} = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
The status field indicates the data state (pending/error/success), while fetchStatus indicates the fetch operation state (fetching/paused/idle).
Query Options
Stale Time
The time in milliseconds after data is considered stale. Defaults to 0.
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5000, // Data is fresh for 5 seconds
})
Set staleTime: Infinity for data that never needs to be refetched, or use the special 'static' value for compile-time static data.
The time in milliseconds that unused/inactive cache data remains in memory. Defaults to 5 minutes.
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
gcTime: 1000 * 60 * 10, // 10 minutes
})
Retry
Control how queries retry on failure:
// Retry 3 times
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
retry: 3,
})
// Retry infinitely
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
retry: true,
})
// Conditional retry
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
retry: (failureCount, error) => {
// Don't retry on 404
if (error.status === 404) return false
// Retry up to 3 times
return failureCount < 3
},
})
Retry Delay
Customize the delay between retries:
import { useQuery } from '@tanstack/react-query'
// Default: exponential backoff (1s, 2s, 4s, ...)
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
})
// Fixed delay
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
retryDelay: 1000, // Always wait 1 second
})
Initial Data
Provide initial data to prevent loading states:
useQuery({
queryKey: ['todo', todoId],
queryFn: () => fetchTodo(todoId),
initialData: () => {
// Use data from another query as initial data
return queryClient
.getQueryData(['todos'])
?.find(todo => todo.id === todoId)
},
})
Queries with initialData start in success status immediately.
Placeholder Data
Show temporary data while the query loads:
useQuery({
queryKey: ['todo', todoId],
queryFn: () => fetchTodo(todoId),
placeholderData: previousData => previousData,
})
placeholderData is never persisted to the cache. For persisted initial data, use initialData.
Enabled Queries
Control when queries run:
function Todos({ userId }) {
// This query will not execute until userId exists
const { data } = useQuery({
queryKey: ['todos', userId],
queryFn: () => fetchTodosByUser(userId),
enabled: !!userId,
})
}
Refetching
Manual Refetch
On Window Focus
On Reconnect
On Mount
function Todos() {
const { data, refetch } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
return (
<div>
<button onClick={() => refetch()}>Refetch</button>
{/* ... */}
</div>
)
}
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
refetchOnWindowFocus: true, // Default
})
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
refetchOnReconnect: true, // Default
})
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
refetchOnMount: true, // Default
})
Polling with Refetch Interval
Automatically refetch at regular intervals:
// Poll every 5 seconds
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
refetchInterval: 5000,
})
// Poll only when window is focused
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
refetchInterval: 5000,
refetchIntervalInBackground: false, // Default
})
// Dynamic interval
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
refetchInterval: (query) => {
return query.state.data?.length > 0 ? 10000 : 5000
},
})
Select Data
Transform or select a portion of query data:
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (data) => {
// Only return completed todos
return data.filter(todo => todo.completed)
},
})
The select function is only called when data exists and is memoized, so it only runs when the data changes.
Dependent Queries
Queries that depend on previous queries:
function User({ userId }) {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
})
const userProjectsEnabled = !!user?.id
const { data: projects } = useQuery({
queryKey: ['projects', user?.id],
queryFn: () => fetchProjectsByUser(user.id),
enabled: userProjectsEnabled,
})
}
Parallel Queries
Multiple queries in the same component run in parallel:
function Overview() {
// These run in parallel
const usersQuery = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
const projectsQuery = useQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
})
const tasksQuery = useQuery({
queryKey: ['tasks'],
queryFn: fetchTasks,
})
}
Dynamic Parallel Queries with useQueries
For a variable number of queries:
import { useQueries } from '@tanstack/react-query'
function Projects({ projectIds }) {
const projectQueries = useQueries({
queries: projectIds.map(id => ({
queryKey: ['project', id],
queryFn: () => fetchProject(id),
})),
})
// Check if all queries are successful
const allSuccess = projectQueries.every(q => q.isSuccess)
}
Query Cancellation
Queries support cancellation via AbortSignal:
useQuery({
queryKey: ['todos'],
queryFn: async ({ signal }) => {
const response = await fetch('/api/todos', { signal })
return response.json()
},
})
When a query is cancelled (e.g., component unmounts), TanStack Query automatically calls abort() on the signal.
Error Handling
Per Query
Global Default
With Error Boundary
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
throwOnError: true, // Throw errors to error boundary
})
const queryClient = new QueryClient({
defaultOptions: {
queries: {
throwOnError: true,
},
},
})
import { ErrorBoundary } from 'react-error-boundary'
function App() {
return (
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Todos />
</ErrorBoundary>
)
}
function Todos() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
throwOnError: true,
})
return <div>{/* ... */}</div>
}
Attach metadata to queries:
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
meta: {
errorMessage: 'Failed to fetch todos',
},
})
Access meta in global callbacks:
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
console.log(query.meta?.errorMessage)
},
}),
})