Introduction to React Query
What is React Query?
React Query (now called TanStack Query) is a powerful data-fetching and state management library for React applications. It simplifies how you fetch, cache, synchronize, and update server state in your React apps.
Unlike traditional state management solutions like Redux, React Query is specifically designed for managing server state, data that originates from an external source and needs to be kept in sync.
Before React Query, developers had to manually handle loading states, error states, caching, background updates, and stale data. This often led to complex, error-prone code scattered across components. React Query provides a declarative approach that handles all of this automatically.
Why Use React Query?
Managing server state is fundamentally different from managing client state. Server state is persisted remotely, requires asynchronous APIs to fetch and update, has shared ownership (other people can change it without your knowledge), and can become stale in your application if you're not careful.
Benefits of React Query:
Automatic Caching - React Query caches your data automatically. When you request the same data again, it returns the cached version instantly while refetching in the background.
Background Updates - Data is automatically refetched in the background when it becomes stale, when the window regains focus, or when the network reconnects.
Deduplication - Multiple components requesting the same data result in only one network request. React Query shares the result across all subscribers.
Optimistic Updates - Update your UI immediately before the server confirms the change, providing a snappy user experience.
Pagination and Infinite Scroll - Built-in support for paginated and infinite scroll interfaces with minimal code.
Key Concepts
Queries are declarative dependencies on asynchronous data sources. They are used for fetching data from a server. The useQuery hook is the primary way to define queries in React Query.
Mutations are used to create, update, or delete data on the server. Unlike queries, mutations are typically triggered by user actions like form submissions or button clicks. The useMutation hook handles mutations.
Query Keys uniquely identify your queries. They are used for caching, refetching, and sharing data across components. Query keys can be simple strings or complex arrays with variables.
Query Client is the core of React Query. It manages the cache, handles garbage collection, and provides methods for interacting with your queries programmatically.
| Feature | Traditional Approach | React Query |
|---|---|---|
| Caching | Manual implementation | Automatic |
| Loading States | useState + useEffect | Built-in |
| Error Handling | try/catch + state | Built-in |
| Background Refetch | Custom logic | Automatic |
| Deduplication | Not available | Automatic |
Tip: React Query is not a replacement for client state management. Use it alongside useState, useReducer, or other state management solutions for local UI state.
Setting Up Your Environment
Prerequisites
Before setting up React Query, you should have Node.js installed on your machine. You'll also need a basic understanding of React fundamentals including components, props, state, and hooks.
Creating a React Project with Vite
We'll use Vite to create our React project because it's fast and has excellent developer experience. Run the following commands in your terminal:
npm create vite@latest react-query-demo -- --template react
cd react-query-demo
npm install
Installing React Query
Install TanStack Query (React Query v5) and its devtools package:
npm install @tanstack/react-query@5.90.20 @tanstack/react-query-devtools@5.91.2
Note: We're using specific versions (5.90.20 for @tanstack/react-query and 5.91.2 for @tanstack/react-query-devtools) which were the latest at the time of writing. This ensures all code examples work correctly regardless of future updates.
Understanding the Packages
@tanstack/react-query - The core library that provides hooks like useQuery and useMutation for fetching and managing server state. It handles caching, background refetching, and synchronization automatically.
@tanstack/react-query-devtools - A development tool that provides a visual interface to inspect and debug your queries. It shows cache state, query status, and timing information. This package is optional but highly recommended for development.
Setting Up the Query Client
The Query Client is the central piece of React Query. It manages the cache and provides the configuration for all queries and mutations. Update your main.jsx file:
// src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import App from './App'
import './index.css'
// Create a client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
retry: 1,
},
},
})
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</React.StrictMode>
)
Important: Always create the QueryClient instance outside of your component (at the module level) as shown above. If you create it inside a component, a new QueryClient will be created on every render, causing the cache to be reset and breaking React Query's caching functionality.
Understanding the Configuration
QueryClient - Creates a new query client instance. You can pass default options that apply to all queries and mutations.
QueryClientProvider - A React context provider that makes the query client available to all components in your application.
staleTime - The time in milliseconds after which data is considered stale. Stale data will be refetched automatically in the background. Default is 0.
retry - The number of times to retry failed queries before giving up. Default is 3.
Tip: Use
npm run devto start the development server. The React Query DevTools will appear as a flower icon in the bottom-right corner of your app.
Want to master React Query by building a real-world, large-scale application from scratch? Check out this comprehensive course.
Your First Query with useQuery
The useQuery Hook
The useQuery hook is the primary way to fetch data in React Query. It takes a configuration object with at least two properties: a unique query key and a query function that returns a promise.
import { useQuery } from '@tanstack/react-query'
const { data, isLoading, error } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
Creating Your First Query
Let's create a component that fetches a list of users from a public API. Create a new file called Users.jsx:
// src/components/Users.jsx
import { useQuery } from '@tanstack/react-query'
// Query function - must return a promise
const fetchUsers = async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/users')
if (!response.ok) {
throw new Error('Failed to fetch users')
}
return response.json()
}
function Users() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
if (isLoading) return <p>Loading users...</p>
if (error) return <p>Error: {error.message}</p>
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name} - {user.email}</li>
))}
</ul>
)
}
export default Users
Code Explanation:
fetchUsers function - This async function makes an HTTP request to the JSONPlaceholder API using the fetch API. It checks if the response is successful (
response.ok), and if not, throws an error. On success, it parses and returns the JSON data.useQuery hook - We destructure three values from useQuery:
data(the fetched users),isLoading(true while fetching for the first time), anderror(contains error details if the request fails).queryKey: ['users'] - This unique identifier is used by React Query to cache and track this specific query. Any component using the same query key will share the cached data.
Conditional rendering - We first check if the data is loading, then if there's an error, and finally render the list of users.
Understanding Query States
React Query provides several state values to help you build robust UIs:
isLoading - True when the query is fetching for the first time and has no cached data.
isFetching - True whenever the query is fetching, including background refetches.
isError - True when the query encountered an error.
isSuccess - True when the query has successfully fetched data.
data - The data returned from the query function (undefined until success).
error - The error object if the query failed (null otherwise).
Note: The query function must throw an error for React Query to treat it as a failure. If you use fetch, remember that it doesn't throw on HTTP errors—you need to check
response.okand throw manually.
Now open App.jsx file and replace its contents:
import Users from './components/Users';
function App() {
return <Users />;
}
export default App;
Now, if you check the application by visiting http://localhost:5173/, you will see the list of users as shown below.
Make sure you have started the application by executing 'npm run dev' command.
Now click on the flower icon, which is displayed in the bottom-right corner, and you will be able to see the data coming from the API with various 'Actions' buttons like Refetch, Invalidate, Reset etc.
Understanding the DevTools Action Buttons
Refetch - Manually triggers a new network request to fetch fresh data from the server, regardless of whether the cached data is stale or not.
Invalidate - Marks the query as stale, which signals React Query that the data needs to be refetched. If the query is currently being rendered, it will refetch in the background.
Reset - Resets the query to its initial state, clearing all cached data and returning to the loading state.
Query Keys Deep Dive
What Are Query Keys?
Query keys are the heart of React Query's caching mechanism. They uniquely identify each query and determine how data is cached and shared across your application. When multiple components use the same query key, they share the same cached data.
Types of Query Keys
String Keys - The simplest form of query key is an array with a single string. Use this for queries that don't depend on any variables:
// Simple string key in an array
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchAllTodos,
})
// Another example
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchAllUsers,
})
Array Keys with Variables - When your query depends on variables like an ID or filter, include them in the query key array:
// Query key with a variable
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
})
// Query key with multiple variables
const { data } = useQuery({
queryKey: ['todos', { status, page }],
queryFn: () => fetchTodos({ status, page }),
})
// Accessing variables in the query function
const { data } = useQuery({
queryKey: ['todo', todoId],
queryFn: ({ queryKey }) => {
const [, id] = queryKey
return fetchTodo(id)
},
})
Understanding const [, id] = queryKey - This is JavaScript array destructuring syntax. The queryKey is an array like ['todo', 5]. The comma before id skips the first element ('todo'), and assigns the second element (5) to the variable id. It's equivalent to writing const id = queryKey[1].
Query Key Best Practices
Be Consistent - Use the same key structure throughout your application.
Be Descriptive - Query keys should describe what data they represent.
Include Dependencies - Any variable that affects the query result should be in the key.
Use Query Key Factories - For complex applications, create factory functions:
// Query key factory pattern
const todoKeys = {
all: ['todos'],
lists: () => [...todoKeys.all, 'list'],
list: (filters) => [...todoKeys.lists(), filters],
details: () => [...todoKeys.all, 'detail'],
detail: (id) => [...todoKeys.details(), id],
}
// Usage
useQuery({ queryKey: todoKeys.all, queryFn: fetchTodos })
useQuery({ queryKey: todoKeys.detail(5), queryFn: () => fetchTodo(5) })
Important: Query keys are serialized deterministically. This means
['todos', { status: 'done' }]and['todos', { status: 'done' }]are considered equal.
Handling Loading and Error States
Building Robust UIs
One of the most common challenges in data fetching is handling the various states your UI can be in. React Query makes this straightforward by providing clear status indicators for every query.
The Status Property
Every query has a status property that can be one of three values:
const { status, data, error } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
// status can be:
// 'pending' - Query has no data yet (initial load)
// 'error' - Query encountered an error
// 'success' - Query has data
Complete Loading States Example
// src/components/UserProfile.jsx
import { useQuery } from '@tanstack/react-query'
function UserProfile({ userId }) {
const { data, isLoading, isFetching, isError, error, refetch } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(res => {
if (!res.ok) throw new Error('User not found')
return res.json()
}),
})
if (isLoading) {
return (
<div className="loading-skeleton">
<div className="skeleton-avatar" />
<div className="skeleton-text" />
</div>
)
}
if (isError) {
return (
<div className="error-container">
<h3>Something went wrong</h3>
<p>{error.message}</p>
<button onClick={() => refetch()}>Try Again</button>
</div>
)
}
return (
<div className="user-profile">
{isFetching && <span className="refetching">Updating...</span>}
<img src={data.avatar} alt={data.name} />
<h2>{data.name}</h2>
<p>{data.email}</p>
</div>
)
}
Error Retry Configuration
React Query automatically retries failed queries. You can customize this behavior:
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
})
// Disable retries for specific errors
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: fetchUser,
retry: (failureCount, error) => {
if (error.status === 404) return false
return failureCount < 3
},
})
Tip: Use
isFetchinginstead ofisLoadingwhen you want to show a subtle indicator for background refetches while still displaying cached data.
Want to Learn More?
You've just scratched the surface of what React Query can do! This tutorial covered the essential foundations—setting up your environment, fetching data with useQuery, understanding query keys, and handling loading and error states.
But there's so much more to explore. React Query offers powerful features that can truly transform how you build React applications:
-
Mutations with
useMutation- Learn how to create, update, and delete data on the server with built-in loading states and error handling - Query Invalidation - Keep your data fresh and synchronized by strategically invalidating cached queries after mutations
- Pagination & Infinite Queries - Handle large datasets efficiently with built-in support for traditional pagination and infinite scroll interfaces
- Optimistic Updates - Provide instant feedback to users by updating the UI immediately, even before the server confirms the change
- Prefetching - Load data before users need it to eliminate loading states and make navigation feel instant
- DevTools & Debugging - Master the React Query DevTools to inspect, debug, and understand your application's data flow
- Best Practices - Discover patterns, tips, and common pitfalls to avoid as you build production-ready applications
Each of these topics builds upon the concepts you've learned here and will take your React Query skills to the next level.
Ready to become a React Query expert? Get the complete React Query Complete Beginner's Guide to access all chapters, detailed code examples, and practical exercises that will help you master server state management in React.
Republic Day Discount Offer - Get All Current + Future Courses, Ebooks, Webinars At Just $15 / ₹1200 Instead Of Regular Price $236 / ₹20,060.
About Me
I'm a freelancer, mentor, full-stack developer working primarily with React, Next.js, and Node.js with a total of 12+ years of experience.
Alongside building real-world web applications, I'm also an Industry/Corporate Trainer training developers and teams in modern JavaScript, Next.js and MERN stack technologies, focusing on practical, production-ready skills.
Also, created various courses with 3000+ students enrolled in these courses.
My Portfolio: https://yogeshchavan.dev/



Top comments (0)