Using Axios with React State and useEffect

Integrating Axios with React involves triggering asynchronous HTTP requests within lifecycle hooks like useEffect and storing the resulting data in component or global state. This article covers how to execute API calls with Axios inside useEffect, manage loading and error states with useState, cancel pending requests during cleanup, and structure the data flow for both local components and global state managers.

Basic Integration: Axios inside useEffect

Because React components render synchronously, asynchronous calls cannot be made directly on the component function itself. Instead, Axios calls are wrapped in an asynchronous function declared inside the useEffect hook.

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

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        setLoading(true);
        const response = await axios.get('https://jsonplaceholder.typicode.com/users');
        setUsers(response.data);
      } catch (err) {
        setError(err.message || 'An error occurred');
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();
  }, []); // Empty dependency array runs the effect once on mount

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

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

export default UserList;

Key Components of the Pattern:

  1. Three State Variables: Managing data, loading, and error ensures predictable UI rendering across all request phases.
  2. Internal Async Function: Declaring const fetchUsers = async () => {...} avoids returning a Promise directly to useEffect, which is not allowed.
  3. Dependency Array: Passing variables in the dependency array (e.g., a dynamic userId) re-triggers the Axios request whenever those values change.

Handling Cleanup and Request Cancellation

When a component unmounts before an Axios request completes, attempting to update state can cause memory leaks or unexpected behavior. Axios integrates with the native AbortController API, which can be hooked into the cleanup function of useEffect.

useEffect(() => {
  const controller = new AbortController();

  const fetchData = async () => {
    try {
      const response = await axios.get('/api/data', {
        signal: controller.signal,
      });
      setData(response.data);
    } catch (err) {
      if (axios.isCancel(err)) {
        // Request was canceled; do not update error state
        return;
      }
      setError(err.message);
    }
  };

  fetchData();

  // Cleanup: Cancel request if the component unmounts
  return () => {
    controller.abort();
  };
}, [dependency]);

Abstracting with Custom Hooks

To eliminate boilerplate across multiple components, Axios logic can be encapsulated into a custom React hook.

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

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

  useEffect(() => {
    const controller = new AbortController();

    const execute = async () => {
      try {
        setLoading(true);
        const res = await axios.get(url, { signal: controller.signal });
        setData(res.data);
      } catch (err) {
        if (!axios.isCancel(err)) {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    };

    execute();

    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
}

Integrating with Global State Management

When fetched data needs to be shared globally: