BLOG POSTS
React Axios Tutorial – Making HTTP Requests

React Axios Tutorial – Making HTTP Requests

Making HTTP requests is fundamental to modern web development, especially when building dynamic React applications that need to fetch, send, and manipulate data from APIs. Axios has become the go-to library for handling HTTP requests in React due to its promise-based architecture, robust error handling, and extensive configuration options. This tutorial will walk you through everything from basic setup to advanced patterns, covering request interceptors, error handling strategies, and performance optimizations that’ll save you headaches in production environments.

What Makes Axios Special

While React’s built-in fetch() API works fine for basic requests, Axios brings several advantages that make it worth the extra dependency. It automatically transforms JSON data, provides better error handling, supports request and response interceptors, and offers built-in protection against XSRF attacks.

Here’s a quick comparison of what you get:

Feature Fetch API Axios
JSON Parsing Manual (.json()) Automatic
Request/Response Interceptors Not built-in Built-in
Request Timeout AbortController needed Simple config option
Error Handling Only network errors HTTP error status codes
Browser Support Modern browsers only Wide compatibility

Setting Up Axios in Your React Project

First things first – get Axios installed in your project:

npm install axios
# or
yarn add axios

Create a basic API service file to keep your requests organized. I usually put this in src/services/api.js:

import axios from 'axios';

const API_BASE_URL = 'https://jsonplaceholder.typicode.com';

const apiClient = axios.create({
  baseURL: API_BASE_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

export default apiClient;

This setup gives you a configured instance that you can reuse throughout your app. The timeout setting is crucial – you don’t want your users waiting forever if an API is down.

Basic HTTP Operations

Let’s dive into the four main HTTP methods you’ll use constantly. Here’s a complete component that demonstrates GET, POST, PUT, and DELETE operations:

import React, { useState, useEffect } from 'react';
import apiClient from '../services/api';

const UserManager = () => {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // GET request - fetch all users
  const fetchUsers = async () => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await apiClient.get('/users');
      setUsers(response.data);
    } catch (err) {
      setError('Failed to fetch users: ' + err.message);
    } finally {
      setLoading(false);
    }
  };

  // POST request - create new user
  const createUser = async (userData) => {
    try {
      const response = await apiClient.post('/users', userData);
      setUsers(prevUsers => [...prevUsers, response.data]);
      return response.data;
    } catch (err) {
      setError('Failed to create user: ' + err.message);
      throw err;
    }
  };

  // PUT request - update existing user
  const updateUser = async (userId, userData) => {
    try {
      const response = await apiClient.put(`/users/${userId}`, userData);
      setUsers(prevUsers => 
        prevUsers.map(user => 
          user.id === userId ? response.data : user
        )
      );
      return response.data;
    } catch (err) {
      setError('Failed to update user: ' + err.message);
      throw err;
    }
  };

  // DELETE request - remove user
  const deleteUser = async (userId) => {
    try {
      await apiClient.delete(`/users/${userId}`);
      setUsers(prevUsers => 
        prevUsers.filter(user => user.id !== userId)
      );
    } catch (err) {
      setError('Failed to delete user: ' + err.message);
      throw err;
    }
  };

  useEffect(() => {
    fetchUsers();
  }, []);

  if (loading) return 
Loading...
; if (error) return
Error: {error}
; return (

Users ({users.length})

{users.map(user => (
{user.name} - {user.email}
))}
); }; export default UserManager;

Advanced Request Configuration

Real-world applications need more sophisticated request handling. Here’s how to set up request and response interceptors for authentication and error handling:

import axios from 'axios';

const apiClient = axios.create({
  baseURL: process.env.REACT_APP_API_URL || 'http://localhost:3001/api',
  timeout: 15000,
});

// Request interceptor - add auth token to requests
apiClient.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('authToken');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    
    // Add request timestamp for debugging
    config.metadata = { startTime: new Date() };
    console.log(`🚀 ${config.method.toUpperCase()} ${config.url}`);
    
    return config;
  },
  (error) => {
    console.error('Request interceptor error:', error);
    return Promise.reject(error);
  }
);

// Response interceptor - handle common errors and logging
apiClient.interceptors.response.use(
  (response) => {
    const duration = new Date() - response.config.metadata.startTime;
    console.log(`✅ ${response.config.method.toUpperCase()} ${response.config.url} - ${duration}ms`);
    return response;
  },
  (error) => {
    const duration = new Date() - error.config?.metadata?.startTime;
    console.log(`❌ ${error.config?.method?.toUpperCase()} ${error.config?.url} - ${duration}ms`);
    
    if (error.response?.status === 401) {
      // Token expired or invalid
      localStorage.removeItem('authToken');
      window.location.href = '/login';
    } else if (error.response?.status === 403) {
      // Forbidden - user doesn't have permission
      console.warn('Access denied to resource');
    } else if (error.response?.status >= 500) {
      // Server error - might want to retry
      console.error('Server error occurred');
    }
    
    return Promise.reject(error);
  }
);

export default apiClient;

Handling Loading States and Errors

One pattern I’ve found incredibly useful is creating a custom hook that manages the request lifecycle. This eliminates repetitive loading and error handling code:

import { useState, useCallback } from 'react';
import apiClient from '../services/api';

const useApiRequest = () => {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [data, setData] = useState(null);

  const execute = useCallback(async (requestConfig) => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await apiClient(requestConfig);
      setData(response.data);
      return response.data;
    } catch (err) {
      const errorMsg = err.response?.data?.message || err.message || 'An error occurred';
      setError(errorMsg);
      throw new Error(errorMsg);
    } finally {
      setLoading(false);
    }
  }, []);

  const reset = useCallback(() => {
    setLoading(false);
    setError(null);
    setData(null);
  }, []);

  return { execute, loading, error, data, reset };
};

// Usage example
const UserProfile = ({ userId }) => {
  const { execute, loading, error, data: user } = useApiRequest();

  useEffect(() => {
    execute({
      method: 'GET',
      url: `/users/${userId}`,
    });
  }, [userId, execute]);

  if (loading) return 
Loading user profile...
; if (error) return
Error loading profile: {error}
; if (!user) return null; return (

{user.name}

{user.email}

); };

File Uploads and FormData

File uploads require special handling. Here’s how to upload files with progress tracking:

const FileUploader = () => {
  const [uploadProgress, setUploadProgress] = useState(0);
  const [uploading, setUploading] = useState(false);

  const uploadFile = async (file) => {
    const formData = new FormData();
    formData.append('file', file);
    formData.append('userId', '123'); // additional data

    setUploading(true);
    setUploadProgress(0);

    try {
      const response = await apiClient.post('/upload', formData, {
        headers: {
          'Content-Type': 'multipart/form-data',
        },
        onUploadProgress: (progressEvent) => {
          const progress = Math.round(
            (progressEvent.loaded * 100) / progressEvent.total
          );
          setUploadProgress(progress);
        },
      });

      console.log('Upload successful:', response.data);
      return response.data;
    } catch (error) {
      console.error('Upload failed:', error);
      throw error;
    } finally {
      setUploading(false);
      setUploadProgress(0);
    }
  };

  const handleFileSelect = (event) => {
    const file = event.target.files[0];
    if (file) {
      uploadFile(file);
    }
  };

  return (
    
{uploading && (
Uploading... {uploadProgress}%
)}
); };

Performance Optimization Strategies

When dealing with high-traffic applications, request optimization becomes crucial. Here are some techniques I use:

  • Request cancellation: Cancel requests when components unmount to prevent memory leaks
  • Response caching: Cache responses to avoid redundant API calls
  • Request deduplication: Prevent multiple identical requests from firing simultaneously
  • Batch requests: Combine multiple requests when possible

Here’s a implementation of request cancellation:

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

const useCancelableRequest = () => {
  const cancelTokenSourceRef = useRef(null);

  const execute = async (requestConfig) => {
    // Cancel previous request if it exists
    if (cancelTokenSourceRef.current) {
      cancelTokenSourceRef.current.cancel('Request cancelled due to new request');
    }

    // Create new cancel token
    cancelTokenSourceRef.current = axios.CancelToken.source();

    try {
      const response = await apiClient({
        ...requestConfig,
        cancelToken: cancelTokenSourceRef.current.token,
      });
      return response.data;
    } catch (error) {
      if (axios.isCancel(error)) {
        console.log('Request cancelled:', error.message);
      } else {
        throw error;
      }
    }
  };

  useEffect(() => {
    return () => {
      // Cleanup on unmount
      if (cancelTokenSourceRef.current) {
        cancelTokenSourceRef.current.cancel('Component unmounted');
      }
    };
  }, []);

  return { execute };
};

Common Pitfalls and Troubleshooting

Over the years, I’ve run into these issues more times than I’d like to admit:

  • CORS errors: Usually a backend configuration issue, not an Axios problem
  • Memory leaks: Forgetting to cancel requests or clear timeouts
  • State updates after unmount: Trying to update state after component is gone
  • Infinite request loops: Missing dependencies in useEffect hooks
  • Token refresh handling: Not properly handling expired authentication tokens

Here’s a robust pattern that handles token refresh automatically:

let isRefreshing = false;
let failedQueue = [];

const processQueue = (error, token = null) => {
  failedQueue.forEach(({ resolve, reject }) => {
    if (error) {
      reject(error);
    } else {
      resolve(token);
    }
  });
  
  failedQueue = [];
};

apiClient.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;

    if (error.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        // Queue the request while token is being refreshed
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject });
        }).then(token => {
          originalRequest.headers.Authorization = `Bearer ${token}`;
          return apiClient(originalRequest);
        });
      }

      originalRequest._retry = true;
      isRefreshing = true;

      try {
        const refreshToken = localStorage.getItem('refreshToken');
        const response = await axios.post('/auth/refresh', {
          token: refreshToken
        });
        
        const newToken = response.data.accessToken;
        localStorage.setItem('authToken', newToken);
        
        processQueue(null, newToken);
        
        originalRequest.headers.Authorization = `Bearer ${newToken}`;
        return apiClient(originalRequest);
      } catch (refreshError) {
        processQueue(refreshError, null);
        localStorage.removeItem('authToken');
        localStorage.removeItem('refreshToken');
        window.location.href = '/login';
        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false;
      }
    }

    return Promise.reject(error);
  }
);

Real-World Integration Examples

Here’s how I typically structure API calls in a production React app with proper error boundaries and loading states:

// services/userService.js
import apiClient from './api';

export const userService = {
  async getUsers(page = 1, limit = 10) {
    const response = await apiClient.get('/users', {
      params: { page, limit }
    });
    return response.data;
  },

  async getUserById(id) {
    const response = await apiClient.get(`/users/${id}`);
    return response.data;
  },

  async createUser(userData) {
    const response = await apiClient.post('/users', userData);
    return response.data;
  },

  async updateUser(id, userData) {
    const response = await apiClient.put(`/users/${id}`, userData);
    return response.data;
  },

  async deleteUser(id) {
    await apiClient.delete(`/users/${id}`);
  }
};

// hooks/useUsers.js
import { useState, useEffect } from 'react';
import { userService } from '../services/userService';

export const useUsers = (page = 1) => {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [hasMore, setHasMore] = useState(true);

  useEffect(() => {
    let isMounted = true;

    const fetchUsers = async () => {
      try {
        setLoading(true);
        const data = await userService.getUsers(page);
        
        if (isMounted) {
          setUsers(data.users);
          setHasMore(data.hasMore);
          setError(null);
        }
      } catch (err) {
        if (isMounted) {
          setError(err.message);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };

    fetchUsers();

    return () => {
      isMounted = false;
    };
  }, [page]);

  return { users, loading, error, hasMore };
};

This pattern separates concerns nicely – your components don’t need to know about API implementation details, and you can easily swap out the backend or add caching layers later.

For larger applications running on robust infrastructure like VPS servers or dedicated servers, you might want to implement more sophisticated patterns like request queuing, retry mechanisms, and circuit breakers to handle high load scenarios gracefully.

The official Axios documentation covers additional advanced topics like custom adapters and detailed configuration options that become important as your application scales.



This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.

This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.

Leave a reply

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