Module 4 · React · Drills
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.
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.
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.
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.
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.
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.
// 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.
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.
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.
// 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.
Click a card to flip it. Say the answer out loud before you flip — that's the rep that builds storage strength.
[] = once on mount; [a] = when a changes; omitted = every render.return from an effect; runs before the next effect and on unmount.loading, error, and data — render them in that priority order.use… that calls other hooks and returns whatever shape you need.Tick each only if you can do it without looking:
useDocuments and can provide/consume a useContext value