Lesson 6 / 7

Fetching Data from an API

import { useState, useEffect } from "react";nnfunction CourseList() {n    const [courses, setCourses] = useState([]);n    const [loading, setLoading] = useState(true);nn    useEffect(() => {n        fetch("/api/courses")n            .then(response => response.json())n            .then(data => {n                setCourses(data);n                setLoading(false);n            });n    }, []);nn    if (loading) return <p>Loading...</p>;nn    return (n        <ul>n            {courses.map(course => <li key={course.id}>{course.title}</li>)}n        </ul>n    );n}

This pattern — useState to hold the data and a loading flag, useEffect with an empty dependency array to fetch once when the component appears — is one of the most common shapes in real React code, combining directly on top of the async JavaScript course’s fetch and Promise coverage.

Handling errors

const [error, setError] = useState(null);nnuseEffect(() => {n    fetch("/api/courses")n        .then(response => {n            if (!response.ok) throw new Error("Request failed");n            return response.json();n        })n        .then(setCourses)n        .catch(err => setError(err.message))n        .finally(() => setLoading(false));n}, []);

A real component needs to handle three states, not just one — loading, error, and success — and render something reasonable for each. Forgetting the error case is a common way a real app quietly breaks whenever a network request fails.

Custom hooks: extracting reusable logic

function useCourses() {n    const [courses, setCourses] = useState([]);n    const [loading, setLoading] = useState(true);nn    useEffect(() => {n        fetch("/api/courses").then(r => r.json()).then(data => {n            setCourses(data);n            setLoading(false);n        });n    }, []);nn    return { courses, loading };n}nnfunction CourseList() {n    const { courses, loading } = useCourses();n    // ...n}

A custom hook is just a regular function, starting with use, that calls other hooks internally — this lets you extract and reuse stateful logic (like this data-fetching pattern) across multiple components without duplicating it.