Managing data in React apps often starts with simple CRUD operations using fetch or Axios. As your app grows, handling loading states, caching, refetching, and error management becomes complex. React Query is a powerful library that simplifies data fetching and state management. In this post, we’ll compare React Query with traditional approaches and show why React Query is a game-changer for modern React development.
Traditional CRUD Operations with Fetch/Axios
Most React apps begin by fetching data with fetch or Axios inside useEffect. Here’s a typical example:
import { useState, useEffect } from 'react';
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => {
setUsers(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
With Axios, the code is similar:
import axios from 'axios';
useEffect(() => {
axios.get('/api/users')
.then(res => setUsers(res.data))
.catch(err => setError(err));
}, []);
Challenges with Fetch/Axios
- Manual loading and error state management
- No built-in caching or refetching
- Hard to synchronize data across components
- Complexity increases with pagination, mutations, and optimistic updates
- No automatic background updates
React Query: The Modern Solution
React Query abstracts away the boilerplate of data fetching, caching, and state management. It provides hooks for queries and mutations, handles loading and error states, and keeps your UI in sync with your backend.
- Automatic Caching: Data is cached and reused across components.
- Background Refetching: Keeps data fresh automatically.
- Built-In Loading/Error States: No need to manage these manually.
- Mutations: Simplifies POST, PUT, DELETE operations.
- Optimistic Updates: Instantly update UI before server response.
- Pagination & Infinite Queries: Easy to implement with built-in hooks.
- Devtools: Visualize query state and cache in development.
Example: Fetching Data with React Query
import { useQuery } from '@tanstack/react-query';
function Users() {
const { data, isLoading, error } = useQuery(['users'], () =>
fetch('/api/users').then(res => res.json())
);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
Mutations with React Query
import { useMutation, useQueryClient } from '@tanstack/react-query';
const queryClient = useQueryClient();
const mutation = useMutation(newUser =>
fetch('/api/users', {
method: 'POST',
body: JSON.stringify(newUser),
headers: { 'Content-Type': 'application/json' }
}).then(res => res.json()),
{
onSuccess: () => {
queryClient.invalidateQueries(['users']);
}
}
);
Why Choose React Query?
- Less boilerplate, more productivity
- Automatic cache and refetching
- Easy to scale for large apps
- Better user experience with instant updates
- Works with fetch, Axios, or any data source
Conclusion
While fetch and Axios are great for simple apps, React Query is the best choice for scalable, maintainable, and performant React applications. It handles the complexity of data fetching so you can focus on building great user experiences.