What an effect is for
Rendering is a calculation: props and state in, elements out, nothing else touched. An effect is the escape hatch for the "nothing else" — synchronising your component with something outside React: a subscription, a timer, the document title, a media player, a network request.
React.useEffect(() => {
// runs AFTER the render is on the screen
const id = setInterval(tick, 1000);
return () => clearInterval(id); // cleanup
}, [delay]); // dependencies
The dependency array
| Written as | Runs |
|---|---|
useEffect(fn) | After every render — almost always a mistake |
useEffect(fn, []) | Once after the first render, cleanup on unmount |
useEffect(fn, [a, b]) | When a or b changes (compared with Object.is) |
It is a list of everything the effect reads. Leaving something out to make the effect run less often is how you get stale values that nobody can reproduce. If the effect runs too often, the answer is to change what it depends on — not to lie about it.
Cleanup is not optional
React runs the cleanup before re-running the effect, and again when the component unmounts. Without it you get two intervals, then four; listeners on elements that no longer exist; and setState on something that has gone.
In development StrictMode mounts, cleans up and mounts again on purpose — so a missing cleanup breaks loudly and immediately, instead of quietly in production.
React.useEffect(() => {
function onResize() { setWidth(window.innerWidth); }
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
You probably do not need an effect
// no
const [shown, setShown] = React.useState([]);
React.useEffect(() => { setShown(doors.filter(f)); }, [doors, query]);
// renders once with the old list, then again with the new one,
// and goes wrong the moment the two disagree
// yes
const shown = doors.filter(f);
| Instead of an effect that… | Do this |
|---|---|
| Computes a value from props or state | Compute it during render |
| Resets state when a prop changes | Change the component key |
| Runs because the user clicked | Put it in the event handler |
| Tells the parent that state changed | Call the parent in the same handler that changed it |
| Sets state from other state | Delete the second state |
An effect that depends on a value
Read it as a statement about synchronisation, not as a lifecycle: "while this component is on screen with this roomId, a connection to that room exists". The cleanup is how that sentence stays true.
React.useEffect(() => {
const socket = connect(roomId);
socket.on("message", add);
return () => socket.close();
}, [roomId]);
// roomId "a" → "b" gives, in order:
// close(a) → connect(b)
Objects and functions as dependencies
Prefer primitives in the array. If a function is genuinely needed, define it inside the effect, or wrap it in useCallback where it is created.
// this effect runs on EVERY render: options is a fresh object each time
const options = { room: roomId };
React.useEffect(() => { connect(options); }, [options]);
// move it inside — now the dependency is the string
React.useEffect(() => {
connect({ room: roomId });
}, [roomId]);
The three timings
useEffect— after the browser has painted. The default, and what you want for anything that can flash for a frame without harm.useLayoutEffect— after the DOM is updated but before paint. For measuring a node and adjusting position in the same frame, so nothing is ever seen wrong. It blocks paint, so use it only for that.useInsertionEffect— for CSS-in-JS libraries inserting rules. You will not write one.
Async inside
// wrong: an async function returns a promise, and React expects a cleanup
React.useEffect(async () => { … }, []);
// right
React.useEffect(() => {
let alive = true;
(async () => {
const data = await load();
if (alive) setData(data);
})();
return () => { alive = false; };
}, []);