The work that does not belong in render
Lessons 1-2 and 3-3 established that rendering should be a pure computation, with the same data in producing the same UI out. The work that does not belong there is starting a network request to load data.
A network request reaches outside the component and has effects beyond returning JSX. Rendering may run many times, so anything that touches the outside world, meaning requests, timers, and subscriptions, has to live somewhere else.
The consequence of getting this wrong is concrete rather than theoretical. A fetch called during render fires again on every render, and if its result lands in state, that state change triggers another render and another fetch, which is an infinite loop that hammers the server.
That somewhere else is useEffect, and this unit is about it.
Side effects and useEffect
A side effect is anything a component does beyond computing its JSX: fetching data, starting a timer, subscribing to a websocket, updating document.title. Effects cannot run during render, render must stay pure. React's home for them is the useEffect hook:
import { useEffect } from "react"; function Profile({ userId }) { useEffect(() => { document.title = "Profile of user " + userId; }, [userId]); return <h1>User {userId}</h1>; }
useEffect(setup, deps) takes a function and a dependency array. React renders first, commits the DOM, and then runs your effect. The deps control how often.
The dependency array, three modes
useEffect(() => { ... }); // no array: after EVERY render useEffect(() => { ... }, []); // empty array: after the FIRST render only useEffect(() => { ... }, [a, b]); // after renders where a or b changed
Two words you will meet constantly around effects: a component mounts when React adds it to the screen for the first time, and unmounts when React removes it, say, the user navigates to a different page. So "after the FIRST render only" means: once, at mount.
React compares each dependency to its value from the previous render with Object.is, the same identity comparison from lesson 4-3. All the same rules apply: a mutated object looks unchanged, a fresh object always looks changed.
The golden rule: every value from the component that the effect reads belongs in the array. Props, state, anything computed from them. Leaving one out means the effect keeps using a stale snapshot of it, one of the hardest bugs to spot in React. Editors with the React lint rules will tell you exactly what is missing, take the suggestion.
Reading an effect with an empty dependency array
function Clock() { const [time, setTime] = useState(""); useEffect(() => { setTime(new Date().toLocaleTimeString()); }, []); return <p>{time}</p>; }
The effect runs once, right after the first render, so the clock shows the mount time and never updates.
An empty dependency array means to run after the first render and then never again, so the time is set exactly once. The component re-renders when setTime stores the value, and that re-render does not run the effect again because the deps did not change.
There is a detail worth noticing in the first paint. time starts as "", so the paragraph is briefly empty before the effect runs and the second render fills it in. That flash is why data-loading components usually render a loading state rather than an empty one.
A live clock needs a setInterval inside the effect so the time updates on a schedule, and starting an interval raises the question of who stops it. That is cleanup, which is the next lesson.
The empty array is also the right choice for plenty of real effects, such as reading a value from storage once, setting up a one-time subscription, or logging a page view. Its meaning is precise: this work depends on nothing that can change.
A missing dependency
function Results({ query }) { const [data, setData] = useState(null); useEffect(() => { loadResults(query).then(setData); }, []); return <List data={data} />; }
The bug is that query is read by the effect and missing from the deps, so new queries never trigger a reload and the effect keeps using the first query forever.
The effect reads query, so query belongs in the array as [query]. With [] the effect runs once, and when the parent passes a new query the component re-renders with the new prop while the data stays whatever the original search returned.
The symptom is a search box that appears to ignore everything after the first search. Nothing throws, the network tab shows one request, and the component looks correct in isolation, which is why this class of bug survives code review.
The golden rule covers every case: every value from the component that the effect reads belongs in the array, including props, state, and anything computed from them. The lint rule that ships with React tells you exactly what is missing, and taking its suggestion is nearly always right.
Note the tempting wrong fix, which is to leave the deps empty and add a comment explaining why. Suppressing the lint warning silences the messenger, and the stale value stays, so the honest options are to add the dependency or to restructure so the effect does not need it.