Beyond render
A component, in essence, computes JSX from props and state. But sometimes you need to do something that is not computing the interface: subscribing to a browser event, starting a timer, changing the tab title, talking to an API... This is called a side effect, because it affects something outside the component.
These actions should not happen during render (render must be pure and may
run several times). That's what the useEffect hook is for: it says
"run this code after painting, to synchronize with something external".
function Title() {
const [n, setN] = useState(0);
useEffect(() => {
document.title = "Clicked " + n + " times";
});
return <button onClick={() => setN(n + 1)}>Click</button>;
}
useEffect receives a function (the effect) that React runs after rendering.
The dependency array
The second argument of useEffect is an array that controls when the
effect runs again:
// 1) No array: on EVERY render
useEffect(() => { /* ... */ });
// 2) Empty array []: only ONCE, when the component MOUNTS
useEffect(() => { /* ... */ }, []);
// 3) With dependencies: on mount and every time one of them CHANGES
useEffect(() => { /* ... */ }, [userId]);
[]→ "run it only on mount". Ideal for setting something up once (a subscription, an initial data fetch).[userId]→ "run it whenuserIdchanges". React compares the array values between renders; if any changed, it re-runs the effect.
Golden rule: include in the dependencies every reactive value (props, state) you use inside the effect. If you omit it, you'll work with stale data.
The cleanup function
Many subscriptions must be undone: a timer must be stopped, a listener must be removed. For that, the effect can return a cleanup function. React runs it before re-running the effect and when the component unmounts.
function Clock() {
const [sec, setSec] = useState(0);
useEffect(() => {
const id = setInterval(() => setSec((s) => s + 1), 1000);
return () => clearInterval(id); // cleanup: stops the interval
}, []);
return <p>{sec} s</p>;
}
Without that cleanup, the interval would stay alive after the component unmounts: a resource leak (and possible errors). Cleanup avoids duplicate subscriptions and leaves the system as it was.
While you render, think: "what needs to be connected?" (the effect) and "what needs to be disconnected?" (the cleanup).