React Hooks – useEffect with Axios

Using useEffect with Axios is a common pattern in React for fetching data from APIs when a component mounts or when certain dependencies change. Here’s a breakdown of how it works, along with examples and best practices:

Understanding useEffect

The useEffect hook in React allows you to perform side effects in functional components. Side effects include data fetching, subscriptions, manually changing the DOM, and more. It takes two arguments:

  1. A function: This is where you put your side effect logic (e.g., your Axios request).
  2. An optional dependency array: This array tells React when to re-run the effect.
    • Empty array []: The effect runs only once after the initial render (similar to componentDidMount in class components). This is common for initial data fetches.
    • No array: The effect runs after every render.
    • Array with dependencies [dep1, dep2]: The effect runs after the initial render and whenever any of the dependencies in the array change.

Understanding Axios

Axios is a popular, promise-based HTTP client for JavaScript. It simplifies making HTTP requests from the browser and Node.js.

Key Features of Axios:

  • Promise-based API.
  • Automatic transformation of JSON data.
  • Support for request and response interceptors.
  • Better error handling.
  • Support for canceling requests.

Basic Data Fetching with useEffect and Axios

Let’s illustrate with a common scenario: fetching a list of posts from a public API.

1. Install Axios:

If you haven’t already, install Axios in your React project:


npm install axios
# or
yarn add axios

2. Example Component:

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

function PostsList() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // Define an async function inside useEffect
    const fetchPosts = async () => {
      try {
        const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
        setPosts(response.data);
        setLoading(false); // Data loaded, set loading to false
      } catch (err) {
        setError(err); // Catch any errors
        setLoading(false); // Stop loading even if there's an error
      }
    };

    fetchPosts(); // Call the async function

    // Optional: Cleanup function for useEffect (e.g., aborting requests)
    // This runs when the component unmounts or before the effect re-runs
    return () => {
      // For instance, if you were using an AbortController for request cancellation
      // controller.abort();
    };
  }, []); // Empty dependency array means this effect runs only once on component mount

  if (loading) {
    return <div>Loading posts...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <h3>{post.title}</h3>
            <p>{post.body}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default PostsList;

Explanation:

  • useState hooks:
    • posts: Stores the fetched data (initialized as an empty array).
    • loading: A boolean to indicate if data is being fetched (initialized as true).
    • error: Stores any error that occurs during the fetch (initialized as null).
  • useEffect hook:
    • The useEffect function is defined.
    • Inside useEffect, we define an async function fetchPosts. This is a common pattern because useEffect itself cannot be async.
    • We use a try...catch block for robust error handling.
    • axios.get('URL') makes the GET request.
    • response.data contains the actual data from the API.
    • setPosts(response.data) updates the state with the fetched data.
    • setLoading(false) and setError(err) update the loading and error states accordingly.
    • fetchPosts(); immediately calls the defined async function.
    • The empty dependency array [] ensures this effect runs only once when the component mounts.

Handling Dependencies and Re-fetching Data

If your data fetching depends on certain props or state variables, you should include them in the dependency array.

Example: Fetching data based on a user ID:

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

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUser = async () => {
      setLoading(true); // Set loading to true every time the effect runs
      setError(null); // Clear previous errors
      try {
        const response = await axios.get(`https://jsonplaceholder.typicode.com/users/${userId}`);
        setUser(response.data);
        setLoading(false);
      } catch (err) {
        setError(err);
        setLoading(false);
      }
    };

    if (userId) { // Only fetch if userId is provided
      fetchUser();
    } else {
      setLoading(false); // If no userId, stop loading
    }

  }, [userId]); // Dependency array: Effect runs when userId changes

  if (loading) {
    return <div>Loading user profile...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  if (!user) {
    return <div>No user selected.</div>;
  }

  return (
    <div>
      <h1>User Profile</h1>
      <p>Name: {user.name}</p>
      <p>Email: {user.email}</p>
      <p>Phone: {user.phone}</p>
    </div>
  );
}

export default UserProfile;

Explanation:

  • The userId prop is included in the dependency array [userId]. This means that whenever userId changes, the useEffect hook will re-run, triggering a new API call to fetch the updated user data.
  • We reset loading to true and error to null before each new fetch to provide accurate feedback to the user.

Best Practices and Considerations

  1. Error Handling: Always include try...catch blocks with your Axios requests to handle potential network errors or API response errors.
  2. Loading States: Use useState to manage loading states (setLoading) so you can display a “Loading…” message to the user while data is being fetched.
  3. Empty Dependency Array []: Use an empty array [] when you want the effect to run only once after the initial render (e.g., for initial data loading).
  4. Dependencies: Be mindful of your useEffect dependencies. If you omit a dependency that the effect relies on, you might run into stale closures or infinite loops.
  5. Cleanup Function: For long-running operations like subscriptions or if you need to cancel an API request (e.g., when a component unmounts before the request completes), return a cleanup function from useEffect. Axios provides AbortController for request cancellation.
useEffect(() => {
  const controller = new AbortController();
  const signal = controller.signal;

  const fetchData = async () => {
    try {
      const response = await axios.get('https://api.example.com/data', { signal });
      // ... handle response
    } catch (error) {
      if (axios.isCancel(error)) {
        console.log('Request cancelled', error.message);
      } else {
        console.error('Error fetching data:', error);
      }
    }
  };

  fetchData();

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

Custom Hooks: For more complex data fetching logic or to reuse fetching logic across multiple components, consider creating custom hooks (e.g., useFetch or useAxios). This promotes reusability and keeps your components cleaner.

// hooks/useAxiosFetch.js
import { useState, useEffect } from 'react';
import axios from 'axios';

const useAxiosFetch = (dataUrl) => {
  const [data, setData] = useState([]);
  const [fetchError, setFetchError] = useState(null);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    let isMounted = true;
    const source = axios.CancelToken.source();

    const fetchData = async (url) => {
      setIsLoading(true);
      try {
        const response = await axios.get(url, {
          cancelToken: source.token
        });
        if (isMounted) {
          setData(response.data);
          setFetchError(null);
        }
      } catch (err) {
        if (isMounted) {
          setFetchError(err.message);
          setData([]);
        }
      } finally {
        isMounted && setIsLoading(false);
      }
    }

    fetchData(dataUrl);

    const cleanUp = () => {
      isMounted = false;
      source.cancel();
    }

    return cleanUp;
  }, [dataUrl]);

  return { data, fetchError, isLoading };
};

export default useAxiosFetch;

// In your component:
// import useAxiosFetch from './hooks/useAxiosFetch';
// const { data, fetchError, isLoading } = useAxiosFetch('https://jsonplaceholder.typicode.com/posts');

By following these guidelines, you can effectively use useEffect with Axios to manage data fetching in your React applications.

Recent Posts