DEV Community

Cover image for React Query Complete Beginner's Guide - TanStack Query v5 + React + Vite
Yogesh Chavan
Yogesh Chavan

Posted on

React Query Complete Beginner's Guide - TanStack Query v5 + React + Vite

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:

  1. 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.

  2. Background Updates - Data is automatically refetched in the background when it becomes stale, when the window regains focus, or when the network reconnects.

  3. Deduplication - Multiple components requesting the same data result in only one network request. React Query shares the result across all subscribers.

  4. Optimistic Updates - Update your UI immediately before the server confirms the change, providing a snappy user experience.

  5. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
)
Enter fullscreen mode Exit fullscreen mode

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 dev to start the development server. The React Query DevTools will appear as a flower icon in the bottom-right corner of your app.

Application Started


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,
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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), and error (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.ok and throw manually.

Now open App.jsx file and replace its contents:

import Users from './components/Users';

function App() {
  return <Users />;
}

export default App;
Enter fullscreen mode Exit fullscreen mode

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.

Users List

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.

Devtool

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,
})
Enter fullscreen mode Exit fullscreen mode

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)
  },
})
Enter fullscreen mode Exit fullscreen mode

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

  1. Be Consistent - Use the same key structure throughout your application.

  2. Be Descriptive - Query keys should describe what data they represent.

  3. Include Dependencies - Any variable that affects the query result should be in the key.

  4. 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) })
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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
  },
})
Enter fullscreen mode Exit fullscreen mode

Tip: Use isFetching instead of isLoading when 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)