Using Axios for Simplified Data Fetching

Data fetching is a critical part of modern web applications. Whether you’re building a dashboard, an e-commerce store, or a social media app, you’ll often need to communicate with APIs to retrieve and send data. In React, there are multiple ways to perform HTTP requests—using the built-in fetch() API, third-party libraries, or GraphQL clients.

Among these, Axios has become one of the most popular and developer-friendly libraries for handling HTTP requests. Axios offers a clean syntax, advanced configuration options, and built-in features that simplify data fetching, error handling, and request management.

In this post, we’ll explore everything about using Axios in React—from basic setup to advanced usage, including interceptors, instance configuration, and clean architecture practices for scalable applications.


What Is Axios?

Axios is a lightweight promise-based HTTP client for making requests from browsers or Node.js. It’s built on top of the XMLHttpRequest (XHR) interface but provides a simpler, modern interface for sending and receiving data.

Key Features of Axios

  • Supports Promises for asynchronous requests.
  • Automatically transforms JSON data.
  • Supports request and response interceptors.
  • Allows request cancellation.
  • Provides easy error handling and timeout control.
  • Supports global configuration and custom instances.

Installing Axios in a React Project

You can install Axios using npm or yarn:

npm install axios

Or using yarn:

yarn add axios

After installation, import Axios into your component or service file:

import axios from 'axios';

Making Your First Axios Request

A simple GET request using Axios looks like this:

import React, { useEffect, useState } from 'react';
import axios from 'axios';

function UserList() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/users')
  .then(response => {
    setUsers(response.data);
  })
  .catch(error => {
    console.error('Error fetching users:', error);
  });
}, []); return (
<div>
  <h2>User List</h2>
  {users.map(user => <p key={user.id}>{user.name}</p>)}
</div>
); } export default UserList;

Here’s what happens step by step:

  1. axios.get() sends a GET request to the provided URL.
  2. The server response is automatically parsed into JSON.
  3. Data is stored in React state using setUsers().
  4. Any error is caught and logged in the catch block.

Performing Different Types of Requests

Axios supports all major HTTP methods:

GET Request

Used for fetching data.

axios.get('/api/products');

POST Request

Used to send new data to the server.

axios.post('/api/products', { name: 'Keyboard', price: 99 });

PUT Request

Used to update an existing resource.

axios.put('/api/products/1', { name: 'Gaming Keyboard', price: 129 });

DELETE Request

Used to delete a resource.

axios.delete('/api/products/1');

PATCH Request

Used for partial updates.

axios.patch('/api/products/1', { price: 119 });

All these methods return promises and can be awaited inside async functions.


Using Axios with Async/Await

Using async/await improves readability compared to .then() chaining.

import React, { useEffect, useState } from 'react';
import axios from 'axios';

function ProductList() {
  const [products, setProducts] = useState([]);

  useEffect(() => {
const fetchData = async () => {
  try {
    const res = await axios.get('https://fakestoreapi.com/products');
    setProducts(res.data);
  } catch (error) {
    console.error('Error fetching products:', error);
  }
};
fetchData();
}, []); return (
<div>
  <h2>Products</h2>
  {products.map(item => (
    <p key={item.id}>{item.title}</p>
  ))}
</div>
); } export default ProductList;

This approach makes your data-fetching code cleaner, synchronous-looking, and easier to debug.


Setting Default Configuration

You can configure Axios defaults globally, so you don’t repeat base URLs or headers across requests.

import axios from 'axios';

axios.defaults.baseURL = 'https://api.example.com';
axios.defaults.headers.common['Authorization'] = 'Bearer token123';
axios.defaults.timeout = 10000;

Now every Axios request automatically uses the base URL, header, and timeout you defined.

Example:

axios.get('/users'); // Calls https://api.example.com/users

Creating an Axios Instance

Instead of using global defaults, you can create an Axios instance for better modularity. This is especially useful when working with multiple APIs or authentication systems.

import axios from 'axios';

const api = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 8000,
  headers: { 'Content-Type': 'application/json' }
});

export default api;

Now you can import this instance anywhere in your app:

import api from './api';

async function fetchUsers() {
  const res = await api.get('/users');
  console.log(res.data);
}

This ensures all requests share the same configuration and makes it easier to manage authentication tokens and interceptors.


Handling Request Headers

You can send custom headers with Axios requests easily.

Example: Sending Authorization Header

axios.get('/dashboard', {
  headers: {
Authorization: 'Bearer myToken',
}, });

Example: Sending Custom Headers with POST

axios.post(
  '/upload',
  { file: 'image.png' },
  { headers: { 'Content-Type': 'multipart/form-data' } }
);

Headers can be dynamically set for each request or globally configured in an Axios instance.


Axios Interceptors

Interceptors allow you to intercept requests and responses before they are handled by then() or catch().

They’re extremely useful for:

  • Automatically attaching authentication tokens.
  • Logging requests/responses.
  • Handling global errors (e.g., 401 unauthorized).

Request Interceptor

import axios from 'axios';

axios.interceptors.request.use(
  config => {
const token = localStorage.getItem('authToken');
if (token) {
  config.headers.Authorization = Bearer ${token};
}
console.log('Outgoing Request:', config.url);
return config;
}, error => Promise.reject(error) );

This runs before every request, ensuring tokens or headers are attached automatically.


Response Interceptor

axios.interceptors.response.use(
  response => {
return response;
}, error => {
if (error.response && error.response.status === 401) {
  console.warn('Unauthorized access - redirecting to login');
}
return Promise.reject(error);
} );

This runs after receiving responses and helps handle errors globally.


Combining Request and Response Interceptors in an Instance

You can attach interceptors to a specific Axios instance rather than the global Axios object.

import axios from 'axios';

const api = axios.create({
  baseURL: 'https://secure-api.com',
});

api.interceptors.request.use(config => {
  const token = sessionStorage.getItem('jwt');
  if (token) {
config.headers.Authorization = Bearer ${token};
} return config; }); api.interceptors.response.use( response => response, error => {
if (error.response.status === 403) {
  alert('Access forbidden');
}
return Promise.reject(error);
} ); export default api;

Now every request through api automatically applies authentication and error handling logic.


Global Error Handling

To handle API errors globally, you can use interceptors or helper functions to display messages or handle retries.

Example:

api.interceptors.response.use(
  res => res,
  error => {
if (!error.response) {
  console.error('Network error or server unreachable');
} else if (error.response.status === 404) {
  console.error('Resource not found');
} else if (error.response.status === 500) {
  console.error('Server error occurred');
}
return Promise.reject(error);
} );

This prevents repetitive try/catch blocks and keeps your code DRY (Don’t Repeat Yourself).


Canceling Requests

Axios provides Cancel Tokens to abort ongoing requests—useful when a component unmounts or when you need to cancel duplicate calls.

Example: Canceling a Request

import React, { useEffect } from 'react';
import axios from 'axios';

function DataFetcher() {
  useEffect(() => {
const source = axios.CancelToken.source();
axios.get('/long-request', { cancelToken: source.token })
  .then(response => console.log(response.data))
  .catch(error => {
    if (axios.isCancel(error)) {
      console.log('Request canceled:', error.message);
    }
  });
return () => {
  source.cancel('Component unmounted, request canceled.');
};
}, []); return <div>Fetching Data...</div>; } export default DataFetcher;

This ensures requests don’t continue after a component unmounts, avoiding memory leaks.


Handling Timeouts

You can define request timeouts globally or per request.

axios.get('/slow-api', { timeout: 5000 })
  .then(res => console.log(res.data))
  .catch(err => {
if (err.code === 'ECONNABORTED') {
  console.log('Request timed out');
}
});

Timeouts protect your application from hanging requests due to slow APIs.


Using Axios in Custom Hooks

To maintain clean architecture, it’s better to encapsulate Axios logic inside a custom hook.

Example: useFetch Hook

import { useState, useEffect } from 'react';
import axios from 'axios';

export function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');

  useEffect(() => {
let isMounted = true;
axios.get(url)
  .then(response =&gt; {
    if (isMounted) {
      setData(response.data);
    }
  })
  .catch(err =&gt; setError(err.message))
  .finally(() =&gt; setLoading(false));
return () =&gt; (isMounted = false);
}, [url]); return { data, loading, error }; }

Using the Hook

import React from 'react';
import { useFetch } from './useFetch';

function Posts() {
  const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts');

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
&lt;ul&gt;
  {data.map(post =&gt; &lt;li key={post.id}&gt;{post.title}&lt;/li&gt;)}
&lt;/ul&gt;
); } export default Posts;

This makes data fetching reusable and declarative.


Posting Form Data with Axios

For handling forms, Axios can send POST requests easily with user inputs.

import React, { useState } from 'react';
import axios from 'axios';

function SignupForm() {
  const [form, setForm] = useState({ name: '', email: '' });

  const handleSubmit = async (e) => {
e.preventDefault();
try {
  const res = await axios.post('/register', form);
  console.log('Registered:', res.data);
} catch (err) {
  console.error('Signup failed:', err);
}
}; return (
&lt;form onSubmit={handleSubmit}&gt;
  &lt;input
    type="text"
    placeholder="Name"
    value={form.name}
    onChange={e =&gt; setForm({ ...form, name: e.target.value })}
  /&gt;
  &lt;input
    type="email"
    placeholder="Email"
    value={form.email}
    onChange={e =&gt; setForm({ ...form, email: e.target.value })}
  /&gt;
  &lt;button type="submit"&gt;Sign Up&lt;/button&gt;
&lt;/form&gt;
); } export default SignupForm;

Axios automatically converts JavaScript objects into JSON unless you specify a different content type.


Uploading Files with Axios

File uploads require setting the Content-Type to multipart/form-data.

import React, { useState } from 'react';
import axios from 'axios';

function FileUploader() {
  const [file, setFile] = useState(null);

  const handleUpload = async () => {
const formData = new FormData();
formData.append('file', file);
try {
  const res = await axios.post('/upload', formData, {
    headers: { 'Content-Type': 'multipart/form-data' }
  });
  console.log('File uploaded:', res.data);
} catch (err) {
  console.error('Upload failed:', err);
}
}; return (
&lt;div&gt;
  &lt;input type="file" onChange={e =&gt; setFile(e.target.files&#91;0])} /&gt;
  &lt;button onClick={handleUpload}&gt;Upload&lt;/button&gt;
&lt;/div&gt;
); } export default FileUploader;

Managing Multiple Requests Simultaneously

Axios provides axios.all() for executing multiple requests in parallel.

import axios from 'axios';

async function fetchMultipleData() {
  try {
const &#91;users, posts] = await axios.all(&#91;
  axios.get('/users'),
  axios.get('/posts')
]);
console.log('Users:', users.data);
console.log('Posts:', posts.data);
} catch (err) {
console.error('Error:', err);
} }

This helps when you need multiple API calls before rendering the page.


Axios with React Query

Although Axios works perfectly standalone, pairing it with React Query provides caching, refetching, and synchronization benefits.

Example:

import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

function useUsers() {
  return useQuery(['users'], async () => {
const res = await axios.get('/users');
return res.data;
}); }

This combination simplifies data management and avoids redundant re-fetching.


Structuring Axios in Large Applications

For scalability, separate Axios logic into dedicated files.

Folder Structure Example

src/
  api/
apiClient.js
userService.js
hooks/
useFetch.js
components/
UserList.jsx

apiClient.js

import axios from 'axios';

const apiClient = axios.create({
  baseURL: 'https://api.myapp.com',
  timeout: 8000,
});

apiClient.interceptors.request.use(config => {
  const token = localStorage.getItem('token');
  if (token) config.headers.Authorization = Bearer ${token};
  return config;
});

apiClient.interceptors.response.use(
  res => res,
  err => {
if (err.response?.status === 403) {
  console.error('Access denied');
}
return Promise.reject(err);
} ); export default apiClient;

userService.js

import apiClient from './apiClient';

export const getUsers = () => apiClient.get('/users');
export const getUserById = id => apiClient.get(/users/${id});
export const createUser = data => apiClient.post('/users', data);

This modular architecture promotes clean, maintainable, and reusable code.


Common Mistakes with Axios

  1. Forgetting await – Leads to unresolved promises.
  2. Missing return in async functions – Prevents data from reaching components.
  3. Not handling unmounted components – Causes memory leaks.
  4. Global interceptor misuse – Affects unrelated requests if not scoped properly.
  5. Ignoring error structures – Axios errors contain multiple layers (error.response, error.message, etc.).

Example: Error Handling with Detailed Logs

try {
  const res = await axios.get('/api/data');
} catch (err) {
  if (err.response) {
console.log('Server responded with error:', err.response.status);
} else if (err.request) {
console.log('No response received:', err.request);
} else {
console.log('Error in request setup:', err.message);
} }

This ensures precise debugging of all failure scenarios.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *