js

How to Simplify React Data Fetching with Axios and React Query

Streamline your React apps by combining Axios and React Query for smarter, cleaner, and more reliable data fetching.

How to Simplify React Data Fetching with Axios and React Query

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

Keywords: react,axios,react query,data fetching,state management



Similar Posts
Blog Image
How to Integrate Prisma with Next.js: Complete Guide for Type-Safe Full-Stack Development

Learn how to integrate Prisma with Next.js for type-safe full-stack development. Build modern TypeScript apps with seamless database connectivity and enhanced DX.

Blog Image
Build High-Performance GraphQL API: NestJS, Prisma & Redis Caching Guide

Learn to build a scalable GraphQL API with NestJS, Prisma ORM, and Redis caching. Master DataLoader, real-time subscriptions, and performance optimization techniques.

Blog Image
Building Event-Driven Microservices with NestJS, RabbitMQ, and MongoDB: Complete Production Guide

Master event-driven microservices with NestJS, RabbitMQ & MongoDB. Complete production guide covering CQRS, Saga patterns, deployment, monitoring & scaling. Build robust distributed systems today!

Blog Image
Complete Guide: Building Type-Safe APIs with tRPC, Prisma, and Next.js in 2024

Learn to build type-safe APIs with tRPC, Prisma, and Next.js. Complete guide covering setup, authentication, deployment, and best practices for modern web development.

Blog Image
How to Secure Your Express.js App with Passport.js Authentication

Learn how to integrate Passport.js with Express.js to build secure, scalable login systems using proven authentication strategies.

Blog Image
Complete NestJS Event-Driven Microservices Guide: RabbitMQ, MongoDB, and Saga Pattern Implementation

Learn to build scalable event-driven microservices with NestJS, RabbitMQ & MongoDB. Master Saga patterns, error handling & distributed systems. Start building today!