Fetching API Data
โš›๏ธ React 18+ ๐ŸŸข Chapter 21 of 39 ๐Ÿ“‚ Phase 09: Data Fetching and APIs ๐Ÿ“… 2026 Edition
๐Ÿ“Œ Covered in this chapter: HTTP Basics ยท fetch() ยท GET Request ยท JSON Response ยท Loading/Error/Empty States ยท Cleanup & Race Conditions
Almost every real app needs data from a server. This chapter combines useState and useEffect โ€” the two previous chapters โ€” to fetch, display, and handle errors from a real API.
1The fetch() + useEffect Pattern
๐Ÿ’ป Example 1: Fetching and Displaying a User List
import { useEffect, useState } from "react";

function Users() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch("/api/users")
      .then((response) => response.json())
      .then((data) => setUsers(data))
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <p>Loading...</p>;

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
๐Ÿ” Why This Structure?

The empty dependency array [] ensures the fetch runs exactly once when the component mounts, not on every re-render (which would trigger endless repeated requests).

2Handling Loading, Error, and Empty States
function Users() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch("/api/users")
      .then((res) => {
        if (!res.ok) throw new Error("Failed to fetch");
        return res.json();
      })
      .then(setUsers)
      .catch((err) => setError(err.message))
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  if (users.length === 0) return <p>No users found.</p>;

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

Just as in Chapter 9, handling all three states explicitly (loading, error, empty) before the success case gives users clear feedback instead of a blank or broken-looking screen.

3Async/Await Inside useEffect
useEffect(() => {
  async function loadUsers() {
    const response = await fetch("/api/users");
    const data = await response.json();
    setUsers(data);
    setLoading(false);
  }
  loadUsers();
}, []);

The useEffect callback itself can't be marked async directly (React expects either nothing or a cleanup function returned, not a Promise), so the standard workaround is defining an async function inside the effect and calling it immediately.

โš ๏ธ Making the useEffect Callback Itself async

Writing useEffect(async () => { ... }, []) directly causes a subtle bug: an async function always returns a Promise, and React expects useEffect to return either nothing or a cleanup function โ€” receiving a Promise instead breaks React's cleanup mechanism. Always define and call a separate async function inside the effect instead, as shown above.

๐Ÿ’ป Hands-on Interactive Practice Challenge

Fetch a list of posts from a placeholder API, showing a loading message while waiting and handling any fetch errors gracefully.

React Practice Challenge โ–ถ Run in Compiler
import { useState, useEffect } from "react";

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

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/posts?_limit=5")
      .then(res => res.json())
      .then(setPosts)
      .catch(err => setError(err.message))
      .finally(() => setLoading(false));
  }, []);

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

  return (
    <ul>
      {posts.map(post => <li key={post.id}>{post.title}</li>)}
    </ul>
  );
}
Run This Challenge in Online JS IDE โ†’
โ“ Frequently Asked Questions (FAQ)

Q Should I use fetch() or a library like Axios?

Both work fine for learning and small projects โ€” fetch() is built into the browser with zero setup, while Axios offers a slightly friendlier API and some extra features. Many production apps eventually reach for dedicated data-fetching libraries like React Query, covered briefly in Chapter 25.

Q What's a race condition in data fetching?

It happens when a component re-fetches (e.g., because a search query changed) before the previous request finishes, and the old, slower request's response arrives after the new one, overwriting it with stale data. Cleanup functions and cancellation tokens (like AbortController) are used to guard against this in production apps.

OC
Written by Our Compiler Technical Editorial Team
Reviewed for accuracy & tested on React 18+ ยท Last updated August 2026