Skip to main content
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:
  1. A unique query key - An array-based identifier for the query
  2. 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.

Build docs developers (and LLMs) love