Lesson 23 +10 XP

Data Fetching with useEffect

Data Fetching with useEffect

Fetching data from API endpoints when components mount is one of the most common applications of useEffect.

Standard Data Fetching Pattern

A robust data-fetching component typically manages three states:

  1. Data State: Holds response data from the API.
  2. Loading State: Indicates whether the fetch request is currently in-flight.
  3. Error State: Captures network failure or HTTP errors.
import { useState, useEffect } from 'react';

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

  useEffect(() => {
    // Define async function inside effect callback
    async function loadUsers() {
      try {
        setLoading(true);
        const response = await fetch('https://jsonplaceholder.typicode.com/users');
        if (!response.ok) {
          throw new Error("HTTP error! status: " + response.status);
        }
        const data = await response.json();
        setUsers(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }

    loadUsers();
  }, []); // Run once on component mount

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

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name} ({user.email})</li>
      ))}
    </ul>
  );
}
⚠️ DocHero Safety Disclaimer

Do NOT make the useEffect argument callback function directly async (useEffect(async () => ...)). useEffect callbacks must return either a cleanup function or nothing. Instead, declare an async function inside the effect callback and invoke it.

Handling Race Conditions with AbortController

If a dependency changes rapidly, responses from earlier network requests can return out of order. Standard browser AbortController can cancel pending fetch calls on cleanup.

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

  fetch("/api/search?q=" + query, { signal: controller.signal })
    .then(res => res.json())
    .then(data => setData(data))
    .catch(err => {
      if (err.name !== 'AbortError') setError(err);
    });

  return () => controller.abort(); // Cancel request if query changes before fetch completes
}, [query]);

Summary & TL;DR

  • Manage loading, error, and data states during API calls.
  • Declare async functions inside the effect callback; never make the top-level effect callback async.
  • Use AbortController in the cleanup function to handle race conditions and cancel pending requests.