Skip to main content

React Query Overview

TanStack Query (formerly React Query) is a powerful library for managing, caching, and synchronizing asynchronous and remote data in React applications. It provides hooks that make fetching, caching, synchronizing, and updating server state simple and efficient.

Why React Query?

React Query eliminates the need to manually manage loading states, error states, and cache invalidation. It provides:
  • Automatic caching - Intelligently caches your data with configurable retention
  • Background refetching - Keeps data fresh automatically
  • Request deduplication - Multiple components using the same query share a single request
  • Optimistic updates - Update UI before server response for better UX
  • Infinite scroll support - Built-in pagination and infinite query support
  • DevTools - Powerful debugging tools for development

Core Concepts

Queries

Queries are declarative dependencies on asynchronous data sources. Use the useQuery hook to fetch data:
import { useQuery } from '@tanstack/react-query'

function Posts() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['posts'],
    queryFn: async () => {
      const response = await fetch('/api/posts')
      return response.json()
    },
  })

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>

  return (
    <ul>
      {data.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Mutations

Mutations are used for create, update, and delete operations. Use the useMutation hook:
import { useMutation, useQueryClient } from '@tanstack/react-query'

function CreatePost() {
  const queryClient = useQueryClient()
  
  const mutation = useMutation({
    mutationFn: (newPost) => {
      return fetch('/api/posts', {
        method: 'POST',
        body: JSON.stringify(newPost),
      })
    },
    onSuccess: () => {
      // Invalidate and refetch
      queryClient.invalidateQueries({ queryKey: ['posts'] })
    },
  })

  return (
    <button onClick={() => mutation.mutate({ title: 'New Post' })}>
      {mutation.isPending ? 'Creating...' : 'Create Post'}
    </button>
  )
}

Query Client

The QueryClient manages all queries and mutations. Wrap your app with QueryClientProvider:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

const queryClient = new QueryClient()

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
    </QueryClientProvider>
  )
}

Key Features

1

Query Keys

Query keys uniquely identify queries. They can be strings or arrays:
// Simple key
queryKey: ['posts']

// With parameters
queryKey: ['posts', { userId: 1 }]

// Nested keys
queryKey: ['posts', postId, 'comments']
2

Stale Time

Configure how long data stays fresh before refetching:
useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  staleTime: 5000, // 5 seconds
})
3

Cache Time

Control how long unused data stays in cache:
useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  gcTime: 10 * 60 * 1000, // 10 minutes
})

Query States

React Query provides detailed status information:
PropertyDescription
isLoadingQuery is fetching for the first time
isPendingQuery has no data yet
isErrorQuery encountered an error
isSuccessQuery has data
isFetchingQuery is fetching (including background refetch)
dataThe actual data from the query
errorError object if query failed
const { data, isLoading, isError, error, isFetching } = useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
})

if (isLoading) return <Spinner />
if (isError) return <ErrorMessage error={error} />

return (
  <div>
    {isFetching && <RefreshIndicator />}
    <PostsList data={data} />
  </div>
)
The difference between isLoading and isPending: isLoading is true only during the first fetch, while isPending is true whenever there’s no data yet.

Automatic Refetching

React Query automatically refetches data in several scenarios:
  • On mount - When a component mounts
  • On window focus - When the user returns to the tab
  • On network reconnect - When internet connection is restored
  • On interval - At a specified interval if configured
useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  refetchOnWindowFocus: true,
  refetchOnMount: true,
  refetchInterval: 30000, // Refetch every 30 seconds
})
Avoid setting very short refetchInterval values in production as this can lead to unnecessary server load and increased costs.

Dependent Queries

Enable queries conditionally based on other data:
function User({ userId }) {
  const { data: user } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  })

  const { data: projects } = useQuery({
    queryKey: ['projects', user?.id],
    queryFn: () => fetchProjects(user.id),
    enabled: !!user?.id, // Only run when user.id exists
  })

  return <div>...</div>
}

Parallel Queries

Fetch multiple queries simultaneously:
function Dashboard() {
  const users = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
  const posts = useQuery({ queryKey: ['posts'], queryFn: fetchPosts })
  const comments = useQuery({ queryKey: ['comments'], queryFn: fetchComments })

  if (users.isLoading || posts.isLoading || comments.isLoading) {
    return <Loading />
  }

  return <div>...</div>
}
Or use useQueries for dynamic parallel queries:
import { useQueries } from '@tanstack/react-query'

function MultipleUsers({ userIds }) {
  const results = useQueries({
    queries: userIds.map((id) => ({
      queryKey: ['user', id],
      queryFn: () => fetchUser(id),
    })),
  })

  return <div>...</div>
}
React Query automatically handles request deduplication, so if multiple components request the same data, only one network request is made.

Next Steps

Installation

Install and configure React Query in your project

Quick Start

Build your first React Query app

TypeScript

Learn about type-safe queries and mutations

DevTools

Debug your queries with React Query DevTools

Build docs developers (and LLMs) love