Queries are the foundation of TanStack Query. A query is a declarative dependency on an asynchronous source of data that is tied to a unique key. Queries are used to fetch, cache, and update data from your server.
Query Basics
A query requires two things:
- A unique query key - An array-based identifier for the query
- A query function - A function that returns a Promise that resolves data or throws an error
import { useQuery } from '@tanstack/react-query'
function usePosts() {
return useQuery({
queryKey: ['posts'],
queryFn: async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/posts')
return await response.json()
},
})
}
Query States
A query can be in one of the following states at any given moment:
Status States
Queries have a status field that indicates the current state:
pending - The query has no data yet and is currently fetching
error - The query encountered an error
success - The query was successful and data is available
function Posts() {
const { status, data, error } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
})
if (status === 'pending') {
return <div>Loading...</div>
}
if (status === 'error') {
return <div>Error: {error.message}</div>
}
return (
<div>
{data.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
)
}
Fetch Status
In addition to the status field, queries also have a fetchStatus that provides additional granularity:
fetching - The query is currently fetching
paused - The query wanted to fetch, but it is paused (usually due to being offline)
idle - The query is not doing anything at the moment
The fetchStatus is independent of the status. A query can be in success status but still have fetching fetchStatus when performing a background refetch.
Derived Boolean States
For convenience, the query result also includes derived boolean flags:
isPending - Equivalent to status === 'pending'
isSuccess - Equivalent to status === 'success'
isError - Equivalent to status === 'error'
isFetching - Equivalent to fetchStatus === 'fetching'
isPaused - Equivalent to fetchStatus === 'paused'
isLoading - Equivalent to isPending && isFetching (first load with no data)
isRefetching - Equivalent to isFetching && !isPending (background refetch)
These flags are derived from the observer’s state computation in queryObserver.ts:560-589:
const result: QueryObserverBaseResult<TData, TError> = {
status,
fetchStatus: newState.fetchStatus,
isPending,
isSuccess: status === 'success',
isError,
isInitialLoading: isLoading,
isLoading,
data,
dataUpdatedAt: newState.dataUpdatedAt,
error,
errorUpdatedAt,
failureCount: newState.fetchFailureCount,
failureReason: newState.fetchFailureReason,
errorUpdateCount: newState.errorUpdateCount,
isFetched: newState.dataUpdateCount > 0 || newState.errorUpdateCount > 0,
isFetchedAfterMount:
newState.dataUpdateCount > queryInitialState.dataUpdateCount ||
newState.errorUpdateCount > queryInitialState.errorUpdateCount,
isFetching,
isRefetching: isFetching && !isPending,
isLoadingError: isError && !hasData,
isPaused: newState.fetchStatus === 'paused',
isPlaceholderData,
isRefetchError: isError && hasData,
isStale: isStale(query, options),
refetch: this.refetch,
}
Query Lifecycle
1. Query Observer Creation
When you use useQuery, a QueryObserver is created that subscribes to changes in the query’s state. From queryObserver.ts:71-89:
constructor(
client: QueryClient,
public options: QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>,
) {
super()
this.#client = client
this.#selectError = null
this.#currentThenable = pendingThenable()
this.bindMethods()
this.setOptions(options)
}
2. Subscription
When the observer gets its first subscriber, it checks if it should fetch on mount (queryObserver.ts:95-107):
protected onSubscribe(): void {
if (this.listeners.size === 1) {
this.#currentQuery.addObserver(this)
if (shouldFetchOnMount(this.#currentQuery, this.options)) {
this.#executeFetch()
} else {
this.updateResult()
}
this.#updateTimers()
}
}
3. Data Fetching
When a fetch is executed, the query transitions through states. The fetch state is defined in query.ts:690-709:
export function fetchState<TQueryFnData, TError, TData, TQueryKey extends QueryKey>(
data: TData | undefined,
options: QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
) {
return {
fetchFailureCount: 0,
fetchFailureReason: null,
fetchStatus: canFetch(options.networkMode) ? 'fetching' : 'paused',
...(data === undefined &&
({
error: null,
status: 'pending',
} as const)),
} as const
}
4. Result Updates
As the query state changes, the observer computes new results and notifies listeners only when tracked properties change (queryObserver.ts:641-697).
Stale Queries
A query is considered “stale” when it’s old enough that it should be refetched. The staleness is determined by:
- The
staleTime option (defaults to 0, meaning data is immediately stale)
- Whether the query has been invalidated
From query.ts:308-323:
isStaleByTime(staleTime: StaleTime = 0): boolean {
// no data is always stale
if (this.state.data === undefined) {
return true
}
// static is never stale
if (staleTime === 'static') {
return false
}
// if the query is invalidated, it is stale
if (this.state.isInvalidated) {
return true
}
return !timeUntilStale(this.state.dataUpdatedAt, staleTime)
}
Set staleTime to control how long data is considered fresh. For data that doesn’t change often, use a higher staleTime to reduce unnecessary refetches.
Background Refetching
Queries automatically refetch in the background under several conditions:
- New instances of the query mount
- Window is refocused (controlled by
refetchOnWindowFocus)
- Network is reconnected (controlled by
refetchOnReconnect)
- Refetch interval is configured (via
refetchInterval)
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5000, // Data is fresh for 5 seconds
refetchOnWindowFocus: true, // Refetch when window regains focus
refetchOnReconnect: true, // Refetch when network reconnects
refetchInterval: 30000, // Refetch every 30 seconds
})
Enabled Queries
Queries can be disabled using the enabled option. This is useful for dependent queries:
function usePost(postId: number) {
return useQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
enabled: !!postId, // Only run when postId is truthy
})
}
Initial Data
You can provide initial data for a query, which will be used immediately while the query fetches in the background:
const { data } = useQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
initialData: () => {
// Use data from another query as initial data
return queryClient
.getQueryData(['posts'])
?.find((post) => post.id === postId)
},
})
Placeholder Data
Placeholder data allows you to show fake data while the real data is loading. Unlike initialData, placeholder data is not persisted to the cache:
const { data } = useQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
placeholderData: (previousData, previousQuery) => {
// Show previous data while fetching new data
return previousData
},
})
Placeholder data is not cached and will be replaced by actual data when it arrives. Use initialData if you want the data to be persisted to the cache.
Query Function Context
Query functions receive a QueryFunctionContext object with useful properties:
queryKey - The query key
signal - An AbortSignal for cancellation
meta - Optional metadata
pageParam - For infinite queries
From types.ts:138-165, the context is defined as:
export type QueryFunctionContext<
TQueryKey extends QueryKey = QueryKey,
TPageParam = never,
> = {
client: QueryClient
queryKey: TQueryKey
signal: AbortSignal
meta: QueryMeta | undefined
pageParam?: unknown
direction?: unknown
}
Type Safety
TanStack Query provides full type safety for your queries:
type Post = {
id: number
title: string
body: string
}
function usePosts() {
return useQuery<Post[], Error>({
queryKey: ['posts'],
queryFn: async (): Promise<Post[]> => {
const response = await fetch('https://api.example.com/posts')
return await response.json()
},
})
}
function Posts() {
const { data } = usePosts()
// data is typed as Post[] | undefined
}
Error Handling
When a query function throws an error, the query enters the error state:
function usePosts() {
return useQuery({
queryKey: ['posts'],
queryFn: async () => {
const response = await fetch('https://api.example.com/posts')
if (!response.ok) {
throw new Error('Failed to fetch posts')
}
return await response.json()
},
retry: 3, // Retry failed requests 3 times
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
})
}
By default, TanStack Query will retry failed queries 3 times with exponential backoff before setting the query to an error state.