Managing data fetching and state in React apps can quickly become complex and repetitive. Whether you're calling REST APIs or GraphQL endpoints, the traditional useEffect
+ fetch
or Axios pattern often leads to duplicated logic, cluttered components, and poor user experience due to missing caching, loading states, or error handling.
That’s where React Query, now officially part of the TanStack, comes in. React Query is a powerful data-fetching library for React that simplifies and supercharges how you fetch, cache, synchronize, and update server state in your applications — all with minimal boilerplate.
In this tutorial, you’ll learn how to:
-
Fetch and display data using
useQuery
-
Handle loading and error states with ease
-
Perform POST, PUT, and DELETE requests with
useMutation
-
Use caching and invalidation to keep your UI in sync
-
Debug queries using React Query Devtools
By the end, you’ll have a solid understanding of how to integrate React Query into your frontend projects, making your code cleaner, more scalable, and more responsive.
What is React Query (TanStack Query)?
React Query, now part of the TanStack library suite, is a powerful tool for managing server state in React applications. Unlike client state (which you handle with tools like useState
or Redux), server state comes from remote sources — APIs, databases, or services — and often changes outside of your app’s control.
React Query provides a set of hooks that:
-
Fetch and cache server data (
useQuery
) -
Update or mutate data (
useMutation
) -
Automatically refetch and sync data in the background
-
Handle loading, error, and success states efficiently
-
Enable features like pagination, prefetching, and stale-time caching
🆚 Traditional Data Fetching vs. React Query
Feature | Traditional Approach | React Query |
---|---|---|
Data Fetching | useEffect + fetch /Axios |
useQuery , useMutation |
Caching | Manual (if any) | Automatic |
Loading/Error State | Must be manually handled | Built-in |
Background Refetching | Manual setup | Automatic |
Performance | Often suboptimal | Optimized by default |
React Query doesn't replace client-side state libraries like Redux or Zustand — instead, it complements them by handling server-side state efficiently.
In short, React Query helps you write less code, build faster UIs, and spend less time debugging data sync issues.
Setting Up the Project
Before diving into React Query’s features, let’s set up a simple React app and install the necessary dependencies.
✅ Prerequisites
Make sure you have the following installed:
-
Node.js (v18 or newer recommended)
-
npm or yarn
-
Basic knowledge of React and functional components
🛠️ 1. Create a New React App
You can use Vite for a faster, modern setup:
npm create vite@latest react-query-demo -- --template react
cd react-query-demo
npm install
Or use Create React App:
npx create-react-app react-query-demo
cd react-query-demo
📦 2. Install React Query and Axios
React Query works with any fetch library. In this tutorial, we’ll use Axios.
npm install @tanstack/react-query axios
You can optionally add React Query Devtools:
npm install @tanstack/react-query-devtools
🌐 3. Choose an API for Demonstration
We’ll use JSONPlaceholder — a free fake REST API — for demonstration purposes.
Example endpoint for fetching posts:
https://jsonplaceholder.typicode.com/posts
🧩 4. Set Up React Query Client
In src/main.jsx
or src/index.js
, wrap your app with the QueryClientProvider
:
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>
);
Optional: Add React Query Devtools inside App.jsx
:
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
function App() {
return (
<>
<h1>React Query Demo</h1>
{/* Your components */}
<ReactQueryDevtools initialIsOpen={false} />
</>
);
}
Basic Data Fetching with useQuery
The useQuery
hook is the core of React Query. It allows you to fetch data, manage caching, and handle loading or error states effortlessly.
Let’s fetch and display a list of posts from JSONPlaceholder.
🧱 1. Create an API Utility
Create a new file src/api/posts.js
:
import axios from 'axios';
const API_URL = 'https://jsonplaceholder.typicode.com';
export const fetchPosts = async () => {
const response = await axios.get(`${API_URL}/posts`);
return response.data;
};
⚓ 2. Use useQuery
to Fetch Posts
In src/App.jsx
, import the necessary hooks and components:
import { useQuery } from '@tanstack/react-query';
import { fetchPosts } from './api/posts';
function App() {
const { data, isLoading, isError, error } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
if (isLoading) return <p>Loading posts...</p>;
if (isError) return <p>Error: {error.message}</p>;
return (
<div>
<h1>Posts</h1>
<ul>
{data.map(post => (
<li key={post.id}>
<strong>{post.title}</strong>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
);
}
export default App;
🧠 How It Works
-
queryKey
: A unique identifier for the cached query (must be an array). -
queryFn
: The async function that fetches the data. -
data
: The fetched data. -
isLoading
,isError
,error
: States that React Query manages for you.
🗂️ Automatic Caching
React Query caches the result of this fetch. If the user navigates away and returns, the cached data will be shown instantly and refreshed in the background.
Displaying the Data (with Optional Pagination or Filtering)
Now that we’re successfully fetching data with useQuery
, let’s improve the user interface and optionally introduce simple pagination or filtering using React state.
📃 Basic List Display (Improved UI)
You can separate the post list into its components for clarity:
function PostList({ posts }) {
return (
<ul style={{ padding: 0 }}>
{posts.map(post => (
<li key={post.id} style={{ marginBottom: '1rem', listStyle: 'none' }}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</li>
))}
</ul>
);
}
export default PostList;
Then, update your App.jsx
:
import { useQuery } from '@tanstack/react-query';
import { fetchPosts } from './api/posts';
import PostList from './components/PostList';
function App() {
const { data: posts, isLoading, isError, error } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
if (isLoading) return <p>Loading posts...</p>;
if (isError) return <p>Error: {error.message}</p>;
return (
<div>
<h1>React Query Post List</h1>
<PostList posts={posts} />
</div>
);
}
export default App;
🔢 Optional: Simple Client-Side Pagination
Here’s how to display posts page by page (e.g., 10 per page):
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { fetchPosts } from "./api/posts";
import PostList from "./components/PostList";
function App() {
const [page, setPage] = useState(1);
const perPage = 10;
const {
data: posts,
isLoading,
isError,
error
} = useQuery({
queryKey: ["posts"],
queryFn: fetchPosts
});
if (isLoading) return <p>Loading posts...</p>;
if (isError) return <p>Error: {error.message}</p>;
const totalPages = Math.ceil(posts.length / perPage);
const paginatedPosts = posts.slice((page - 1) * perPage, page * perPage);
return (
<div>
<h1>Paginated Posts</h1>
<PostList posts={paginatedPosts} />
<div style={{ marginTop: "1rem" }}>
<button
onClick={() => setPage((p) => Math.max(p - 1, 1))}
disabled={page === 1}
>
Previous
</button>
<span style={{ margin: "0 1rem" }}>
Page {page} of {totalPages}
</span>
<button
onClick={() => setPage((p) => Math.min(p + 1, totalPages))}
disabled={page === totalPages}
>
Next
</button>
</div>
</div>
);
}
export default App;
Mutations with useMutation
So far, we've only fetched (read-only) data. But what if you want to create, update, or delete resources? That’s where the useMutation
hook shines.
Let’s walk through how to add a new post using useMutation
, and then automatically refetch or update the list when done.
🧱 1. Add a Create Post API Function
In src/api/posts.js
, add the following:
export const createPost = async (newPost) => {
const response = await axios.post(`${API_URL}/posts`, newPost);
return response.data;
};
✍️ 2. Create a Post Form Component
Let’s create a simple form to add a new post:
// src/components/PostForm.jsx
import { useState } from 'react';
function PostForm({ onAdd }) {
const [title, setTitle] = useState('');
const [body, setBody] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
onAdd({ title, body });
setTitle('');
setBody('');
};
return (
<form onSubmit={handleSubmit} style={{ marginBottom: '2rem' }}>
<h2>Create a New Post</h2>
<input
type="text"
placeholder="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/><br />
<textarea
placeholder="Body"
value={body}
onChange={(e) => setBody(e.target.value)}
required
rows={4}
/><br />
<button type="submit">Add Post</button>
</form>
);
}
export default PostForm;
⚓ 3. Use useMutation
in App
Update App.jsx
:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { fetchPosts, createPost } from './api/posts';
import PostList from './components/PostList';
import PostForm from './components/PostForm';
function App() {
const queryClient = useQueryClient();
const { data: posts, isLoading, isError, error } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
const mutation = useMutation({
mutationFn: createPost,
onSuccess: () => {
// Invalidate and refetch the posts query
queryClient.invalidateQueries(['posts']);
},
});
const handleAddPost = (newPost) => {
mutation.mutate(newPost);
};
if (isLoading) return <p>Loading posts...</p>;
if (isError) return <p>Error: {error.message}</p>;
return (
<div>
<h1>React Query CRUD</h1>
<PostForm onAdd={handleAddPost} />
{mutation.isPending && <p>Creating post...</p>}
{mutation.isError && <p>Error: {mutation.error.message}</p>}
{mutation.isSuccess && <p>Post created!</p>}
<PostList posts={posts} />
</div>
);
}
export default App;
🔁 What’s Happening:
-
useMutation
triggers a POST request to add data. -
On success, we invalidate the
['posts']
query so it’s automatically refetched. -
You can show feedback using
mutation.isPending
,mutation.isSuccess
, etc.
Devtools for React Query
React Query comes with a powerful set of developer tools to help you inspect your queries, mutations, and cache in real time.
This section will show you how to add and use React Query Devtools to improve your development workflow.
📦 1. Install the Devtools (if you haven’t already)
If you skipped this earlier, install the package:
npm install @tanstack/react-query-devtools
🧩 2. Add Devtools to Your App
Update App.jsx
:
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
function App() {
// ... your existing logic
return (
<div>
{/* Your app components */}
<PostForm onAdd={handleAddPost} />
{mutation.isPending && <p>Creating post...</p>}
{mutation.isError && <p>Error: {mutation.error.message}</p>}
{mutation.isSuccess && <p>Post created!</p>}
<PostList posts={posts} />
{/* Devtools */}
<ReactQueryDevtools initialIsOpen={false} />
</div>
);
}
🧠 What You Can Do with Devtools:
-
See all active queries and their statuses (fresh, stale, fetching, etc.)
-
Refetch queries manually
-
Inspect cache data and query keys
-
Debug errors and background fetching behavior
💡 Devtools are development-only by default and won’t be included in your production builds.
Advanced Features (Optional but Powerful)
Once you’ve mastered the basics, React Query offers many advanced features to help you build efficient, responsive, and user-friendly apps.
Let’s look at some of the most useful capabilities:
🔁 1. Query Invalidation and Background Refetching
React Query automatically refetches stale data when:
-
A component mounts
-
A window refocuses
-
A query is invalidated manually
You can configure this behavior:
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 1000 * 60 * 5, // 5 minutes
refetchOnWindowFocus: false,
});
-
staleTime
: How long the data stays “fresh” before becoming stale -
refetchOnWindowFocus
: Disable background refetching when switching tabs
🔗 2. Dependent Queries
If one query depends on the result of another (e.g., fetching user details by ID), you can conditionally enable queries:
useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId, // Only run if userId exists
});
🔄 3. Optimistic Updates
You can optimistically update the UI before the server confirms success. This improves perceived speed.
useMutation({
mutationFn: updatePost,
onMutate: async (updatedPost) => {
await queryClient.cancelQueries(['posts']);
const previousPosts = queryClient.getQueryData(['posts']);
queryClient.setQueryData(['posts'], old =>
old.map(post => post.id === updatedPost.id ? updatedPost : post)
);
return { previousPosts };
},
onError: (err, updatedPost, context) => {
queryClient.setQueryData(['posts'], context.previousPosts);
},
onSettled: () => {
queryClient.invalidateQueries(['posts']);
},
});
📦 4. Prefetching Queries
You can fetch data before a component mounts using queryClient.prefetchQuery
— useful for navigation or hover actions.
queryClient.prefetchQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
});
These features allow you to fine-tune performance and user experience, especially in large-scale or dynamic apps.
Best Practices for Using React Query
React Query is powerful, but like any tool, it works best when used thoughtfully. Here are some best practices to help you build clean, maintainable, and scalable applications:
📁 1. Separate API Logic from UI
Keep your API functions (fetchPosts
, createPost
, etc.) in a separate file (e.g., src/api/
) to keep components clean and reusable.
// Good structure
src/
├── api/
│ └── posts.js
├── components/
│ ├── PostForm.jsx
│ └── PostList.jsx
🧠 2. Use Stable queryKey
Arrays
Always use an array for queryKey
. It helps React Query properly cache and refetch data.
useQuery({
queryKey: ['posts'], // ✅ good
...
});
You can include dynamic parts too:
useQuery({
queryKey: ['post', postId],
...
});
🕰️ 3. Adjust Stale Time Strategically
Use staleTime
to avoid unnecessary refetching:
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 1000 * 60 * 2, // 2 minutes
});
This improves performance while keeping data fresh enough.
🔁 4. Invalidate Queries After Mutations
Always invalidate or update the query cache when performing mutations:
onSuccess: () => {
queryClient.invalidateQueries(['posts']);
}
This ensures the UI stays in sync with server data.
⚙️ 5. Wrap with QueryClientProvider
Once
Wrap your entire app in a single QueryClientProvider
— usually at the root level (main.jsx
or index.js
).
Avoid wrapping individual components separately.
🧪 6. Use Devtools in Development
Don’t forget to add <ReactQueryDevtools />
when building and debugging. It saves a ton of time.
📉 7. Avoid Over-Fetching
Use conditional fetching (enabled: false
) or staleTime
to avoid unnecessary API calls.
With these practices, you’ll write better code, reduce bugs, and create faster, more responsive applications.
Conclusion
React Query (TanStack Query) is a game-changer for handling asynchronous server state in modern React applications. It eliminates much of the repetitive boilerplate code involved in fetching and managing data while offering powerful features like:
-
Automatic caching and background updates
-
Mutation handling with rollback and optimistic updates
-
Built-in loading, error, and success states
-
Developer tools for easy debugging
-
Advanced capabilities like prefetching and dependent queries
In this tutorial, you learned how to:
-
Set up React Query in a React project
-
Fetch and cache data using
useQuery
-
Perform create operations with
useMutation
-
Handle state updates and invalidation
-
Use Devtools and advanced strategies to optimize performance
Whether you're building a simple blog or a complex dashboard, React Query helps you build faster, cleaner, and more maintainable applications.
You can get the full source code on our GitHub.
That's just the basics. If you need more deep learning about React.js, React Native, or related, you can take the following cheap course:
- React JS with Redux Certification Training
- React Projects Course: Build Real World Projects
- React Testing with Jest / Vitest - TypeScript - 2025
- React Supabase CRUD App
- React JS & Firebase Complete Course (incl. Chat Application)
- Build the original Instagram with React Native & Firebase
- Master React Native Animations
- Learn How To Create React Native Application & WordPress Api
- Build React Native Apps for Android and iOS
- The Complete ChatGPT with React Native - Mobile Application
Thanks!