Module 4 · React · Drills

Drills: Hooks

Reading hooks and writing hooks are different skills. Type every one of these in a real component — spin up a Vite app or use the React playground — and watch it run before you reveal the solution.

How to use this page Each drill is a small task. Attempt it first, run it in a component, then click “Show solution” to compare. Open the browser console — half of learning hooks is watching when things log. Tick each box as you go; your progress is saved in this browser.

A · Warm-up reps Basic

Drill 1 useEffect

Write a component with a useEffect that runs once on mount and logs "mounted" to the console. Prove it doesn't log again when you click a counter button.

Show solution
function Mounter() {
  const [n, setN] = useState(0);
  useEffect(() => {
    console.log("mounted");   // fires once, not on every click
  }, []);                       // empty deps = mount only
  return <button onClick={() => setN(n + 1)}>Clicked {n}</button>;
}

The empty [] is what pins it to mount. Remove it and it logs on every render — try that to feel the difference.

Drill 2 three-state fetch

Fetch a list from http://localhost:8000/documents and render a loading message, then an error message on failure, then the list. Track all three states.

Show solution
function DocList() {
  const [docs, setDocs] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch("http://localhost:8000/documents")
      .then((r) => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
      .then(setDocs)
      .catch((e) => setError(e.message))
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <p>Loading…</p>;
  if (error)   return <p>Error: {error}</p>;
  return <ul>{docs.map((d) => <li key={d.id}>{d.title}</li>)}</ul>;
}

Guard clauses in priority order — loading, error, data — are the whole pattern. The key on each li is required.

Drill 3 cleanup

Start a setInterval that ticks every second in a useEffect, and add a cleanup function that clears it. Confirm in the console that unmounting stops the ticks.

Show solution
function Ticker() {
  useEffect(() => {
    const id = setInterval(() => console.log("tick"), 1000);
    return () => clearInterval(id);   // cleanup runs on unmount
  }, []);
  return <p>Watch the console.</p>;
}

Without the cleanup, the interval keeps firing after the component is gone — a classic memory leak. Every subscription you start, you tear down.

B · Stretch Intermediate

Drill 4 custom hook

Extract a useToggle(initial) custom hook that returns the current boolean and a function to flip it. Use it in a component to show/hide a panel.

Show solution
// useToggle.js
export function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = () => setOn((v) => !v);   // updater form is safest
  return [on, toggle];
}

// usage
function Panel() {
  const [open, toggle] = useToggle();
  return (
    <>
      <button onClick={toggle}>{open ? "Hide" : "Show"}</button>
      {open && <p>Now you see me.</p>}
    </>
  );
}

A custom hook is just a function starting with use that calls other hooks. It returns whatever shape is handy — an array here, mirroring useState.

Drill 5 useContext

Create an AuthContext, provide a user object at the top of the tree, and consume it in a deeply nested child with useContext — no prop drilling.

Show solution
const AuthContext = createContext(null);

function App() {
  const [user] = useState({ name: "Sam" });
  return (
    <AuthContext.Provider value={user}>
      <Layout />
    </AuthContext.Provider>
  );
}

function Greeting() {            // nested any depth below App
  const user = useContext(AuthContext);
  return <p>Hello {user.name}</p>;
}

Provider wraps the tree; any descendant calls useContext to read the value directly. No intermediate component has to pass user along.

C · Build challenge Build

Mini-project Write useDocuments(token): a custom hook that fetches DocChat's documents from FastAPI and returns { docs, loading, error }. Send the JWT in the Authorization header and ignore late responses with a cleanup flag. This is the exact hook your DocChat UI imports.

Build · useDocuments

Requirements: starts in loading, sets error on a non-2xx response, re-fetches when token changes, and never sets state after the component unmounts.

Show solution
// useDocuments.js
import { useState, useEffect } from "react";

export function useDocuments(token) {
  const [docs, setDocs] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let active = true;
    setLoading(true);
    setError(null);
    fetch("http://localhost:8000/documents", {
      headers: { Authorization: `Bearer ${token}` },
    })
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then((data) => active && setDocs(data))
      .catch((err) => active && setError(err.message))
      .finally(() => active && setLoading(false));

    return () => { active = false; };   // cleanup: drop a late response
  }, [token]);                          // re-run when token changes

  return { docs, loading, error };
}

The active flag is the pro touch: if the component unmounts (or token changes) mid-flight, the cleanup sets active = false so the resolved promise can't call setState on a dead component. [token] in the deps makes it re-fetch on login/logout.

D · Rapid recall Flashcards

Click a card to flip it. Say the answer out loud before you flip — that's the rep that builds storage strength.

The two Rules of Hooks?
Call them only at the top level, and only from components or other hooks.
click to flip
What does the deps array control?
When the effect re-runs. [] = once on mount; [a] = when a changes; omitted = every render.
click to flip
What is a cleanup function?
The function you return from an effect; runs before the next effect and on unmount.
click to flip
The three-state fetch pattern?
Track loading, error, and data — render them in that priority order.
click to flip
When do you reach for useContext?
To share global data (user, theme, token) without prop drilling through every layer.
click to flip
What makes a custom hook?
A function named use… that calls other hooks and returns whatever shape you need.
click to flip

E · Self-check before moving on

Tick each only if you can do it without looking:

Next All ticked? You can drive React from live backend data — the core of every screen in DocChat. Next we take this server-side and meet the framework that ties it all together: Module 5 — Next.js App Router Core.