Learn / Frameworks / React / Data Fetching and Routing

React · Lesson 7 of 7

Data Fetching and Routing

Load data with loading and error states, then add pages with React Router.

  • Intermediate
  • 18 min read
  • 3 objectives

Before this lessonLesson 6: Custom Hooks, Refs and Context

What you will learn

  • Model loading/error/success
  • Cancel stale requests
  • Add client-side routes

Almost every real app loads data from an API. A robust component must handle three situations, not one: loading, error and success. Skipping the first two is why many apps flash blank screens or crash on a network hiccup.

A fetch hook with all three states

function useFetch(url) {
  const [state, setState] = useState({ status: "loading", data: null, error: null });

  useEffect(() => {
    const controller = new AbortController();
    setState({ status: "loading", data: null, error: null });

    fetch(url, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then((data) => setState({ status: "success", data, error: null }))
      .catch((error) => {
        if (error.name !== "AbortError")
          setState({ status: "error", data: null, error });
      });

    return () => controller.abort();     // cancel when url changes or unmounting
  }, [url]);

  return state;
}

AbortController cancels the in-flight request when the component unmounts or url changes, preventing an old response from overwriting newer data.

Using it

function Users() {
  const { status, data, error } = useFetch("https://jsonplaceholder.typicode.com/users");

  if (status === "loading") return <p>Loading...</p>;
  if (status === "error") return <p role="alert">Failed: {error.message}</p>;

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

Mutations

Sending data (POST, PUT, DELETE) usually happens in an event handler, not an effect.

async function createUser(name) {
  const res = await fetch("/api/users", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ name }),
  });
  if (!res.ok) throw new Error("Could not save");
  return res.json();
}

Use a data library when it grows

Hand-rolled fetching does not cache, deduplicate requests, retry or refetch in the background. Libraries such as TanStack Query or SWR provide all of that. The pattern stays the same: a hook returns { data, isLoading, error }.

import { useQuery } from "@tanstack/react-query";

function Users() {
  const { data, isLoading, error } = useQuery({
    queryKey: ["users"],
    queryFn: () => fetch("/api/users").then((r) => r.json()),
  });
  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error</p>;
  return data.map((u) => <p key={u.id}>{u.name}</p>);
}

Routing

Single-page apps show different screens without full page loads. React Router maps URLs to components.

npm install react-router-dom
import { BrowserRouter, Routes, Route, Link, useParams } from "react-router-dom";

function UserPage() {
  const { id } = useParams();
  return <h2>User {id}</h2>;
}

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link> | <Link to="/users/42">User 42</Link>
      </nav>
      <Routes>
        <Route path="/" element={<h1>Home</h1>} />
        <Route path="/users/:id" element={<UserPage />} />
        <Route path="*" element={<p>Not found</p>} />
      </Routes>
    </BrowserRouter>
  );
}

Use <Link> instead of <a> for internal navigation so the page does not reload.

// Write your solution here
Course completeYou finished ReactReview the full course or pick your next one.