Learn / Frameworks / React / Data Fetching and Routing

Intermediate 18 min

Data Fetching and Routing

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

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.

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.

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.

Where to go next

Ready for more? Learn a framework built on React, such as Next.js, for server rendering and file-based routing, and TypeScript for safer props.

Try it yourself

Fetch a list of posts from https://jsonplaceholder.typicode.com/posts, show a loading message and an error message, and add a text box that filters posts by title as you type.

Show solution
function Posts() {
  const { status, data, error } = useFetch("https://jsonplaceholder.typicode.com/posts");
  const [q, setQ] = useState("");
  if (status === "loading") return <p>Loading...</p>;
  if (status === "error") return <p>{error.message}</p>;
  const shown = data.filter((p) => p.title.toLowerCase().includes(q.toLowerCase()));
  return (
    <>
      <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Filter" />
      <ul>{shown.map((p) => <li key={p.id}>{p.title}</li>)}</ul>
    </>
  );
}