useEffect — Side Effects & Data Fetching
What Effects Are For
Rendering must be pure: same props/state → same JSX, no side work. But real apps need to talk to the outside world — fetch data, set timers, subscribe to events, touch localStorage. useEffect runs that code after render:
import { useState, useEffect } from "react";
function Clock() {
const [time, setTime] = useState(new Date());
useEffect(() => {
const id = setInterval(() => setTime(new Date()), 1000);
return () => clearInterval(id); // cleanup — runs on unmount
}, []); // [] = run once after first render
return <p>{time.toLocaleTimeString()}</p>;
}The Dependency Array — Three Modes
useEffect(() => { ... }); // no array: after EVERY render (rarely right)
useEffect(() => { ... }, []); // empty: once, after the first render
useEffect(() => { ... }, [query]); // after renders where query CHANGEDRule: every reactive value used inside the effect belongs in the array. The ESLint plugin (react-hooks/exhaustive-deps) enforces this — trust it.
Cleanup
Return a function and React runs it before the next effect run and on unmount. Anything you start, you stop:
useEffect(() => {
function onResize() { setWidth(window.innerWidth); }
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);Data Fetching — the Full Pattern
function CourseList({ track }) {
const [courses, setCourses] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
async function load() {
setIsLoading(true);
setError(null);
try {
const res = await fetch(`/api/courses?track=${track}`, {
signal: controller.signal,
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
setCourses(await res.json());
} catch (err) {
if (err.name !== "AbortError") setError(err.message);
} finally {
setIsLoading(false);
}
}
load();
return () => controller.abort(); // cancel if track changes mid-flight
}, [track]); // refetch when track changes
if (isLoading) return <Spinner />;
if (error) return <p>Error: {error}</p>;
return <ul>{courses.map(c => <li key={c.id}>{c.title}</li>)}</ul>;
}The abort cleanup prevents the classic race: user switches track quickly, the slow old response lands last and overwrites the new data.
When NOT to Use an Effect
// WRONG — derived state doesn't need an effect
useEffect(() => {
setFullName(first + " " + last);
}, [first, last]);
// RIGHT — just compute it during render
const fullName = first + " " + last;Also wrong: effects that only respond to a button click (put the code in the event handler), and chains of effects setting state that triggers other effects. Effects are for synchronising with external systems — if no external system is involved, you probably don’t need one.
(Production apps usually move fetching into a library — TanStack Query — or a framework loader. But they all build on exactly this pattern, and you’ll write plenty of raw effects regardless.)