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.