I was building a dashboard recently, a typical React app with charts, tables, and user lists. Every component needed data. I found myself drowning in useState for loading and error states, writing useEffect hooks for fetching, and trying to manually stitch together a caching system. It was messy. Then, I combined two tools I already knew—Axios for HTTP calls and React Query for state management. The result wasn’t just an improvement; it changed how I think about data in my apps. Let’s look at how you can connect these tools to make your data fetching simple, smart, and reliable.
Think of Axios as your reliable courier. It’s great at making HTTP requests. You can set default headers, handle errors in one place, and cancel requests if a user navigates away. React Query, on the other hand, is like a brilliant librarian. It doesn’t fetch the data itself, but it knows what data you have, when it’s old, and when to get fresh copies. It manages the complex state—loading, success, error, and caching—so you don’t have to. When you make Axios the courier that React Query uses, you get the best of both.
Why combine them? Because alone, each has a gap. Axios fetches but leaves state management to you. React Query manages state but needs a fetcher function. Together, they cover the entire cycle. You configure Axios once with your base URL and interceptors. Then, you tell React Query to use your Axios instance for all its fetching. This setup gives you a centralized, powerful system.
Let’s start with the Axios setup. You create an instance with your common settings. This is where you set authentication tokens or handle global errors.
// apiClient.js
import axios from 'axios';
const apiClient = axios.create({
baseURL: 'https://api.myapp.com',
timeout: 10000,
});
// Add a request interceptor to inject a token
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Add a response interceptor for global error handling
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Handle unauthorized access
console.log('Redirect to login');
}
return Promise.reject(error);
}
);
export default apiClient;
Now, how does React Query use this? You provide a function. This function uses your Axios client to make the request. React Query will call this function when it needs data.
// useUserData.js
import { useQuery } from '@tanstack/react-query';
import apiClient from './apiClient';
const fetchUsers = async () => {
const { data } = await apiClient.get('/users');
return data; // React Query will cache this 'data'
};
export const useUsers = () => {
return useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
staleTime: 5 * 60 * 1000, // Data is fresh for 5 minutes
});
};
In your component, using the data becomes incredibly clean. Do you see the boilerplate that’s missing? No useState for loading or error. React Query provides that.
// UserComponent.jsx
import { useUsers } from './useUserData';
function UserList() {
const { data: users, isLoading, error } = useUsers();
if (isLoading) return <div>Loading users...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
What happens when you need to send data, like a POST request? You use React Query’s useMutation with Axios. Mutations are for creating, updating, or deleting data. React Query can then invalidate related queries, so your UI updates automatically.
// useAddUser.js
import { useMutation, useQueryClient } from '@tanstack/react-query';
import apiClient from './apiClient';
const addUser = async (newUser) => {
const { data } = await apiClient.post('/users', newUser);
return data;
};
export const useAddUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: addUser,
onSuccess: () => {
// Invalidate the 'users' query so it refetches
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
};
This pattern is powerful. Your Axios interceptors can handle refreshing an expired token, and the failed request will automatically retry with the new token. React Query can refetch data in the background when a user refocuses the browser tab. Have you ever had a tab open for a long time and seen stale data? This combination fixes that.
The real benefit is in complex scenarios. Imagine a dashboard with multiple widgets. With traditional fetching, each widget might fire its own request on mount, leading to duplicate calls. React Query with a shared Axios client deduplicates these requests automatically. The first call goes out, and subsequent calls for the same data get the same promise. This saves network traffic and prevents race conditions.
Pagination and infinite scroll also become simpler. React Query has built-in hooks like useInfiniteQuery. You write your Axios-based fetcher that accepts page information, and React Query manages the page state, caching each page of results.
const fetchProjects = async ({ pageParam = 1 }) => {
const { data } = await apiClient.get(`/projects?page=${pageParam}`);
return data;
};
const { data, fetchNextPage } = useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
getNextPageParam: (lastPage) => lastPage.nextPage,
});
So, what’s the final picture? You configure Axios once. You define your fetcher functions. Then, you use simple hooks in your components. The messy state logic, the caching headaches, the error handling boilerplate—it all disappears. You’re left with declarative code that describes what data you need, not the intricate steps of how to get and manage it.
This approach has fundamentally improved how I build features. I spend less time wiring up data pipelines and more time on the user experience. It makes the code more robust and easier to reason about. If you’re tired of managing loading spinners and cache logic manually, this integration is worth your time.
Give this pattern a try in your next React project. Start by setting up your Axios client, then wrap your first API call with useQuery. The reduction in complexity is immediate. If you found this breakdown helpful, or have your own tips on managing server state, share your thoughts in the comments below. Let’s keep the conversation going.
As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva