A8 · Part 3 · Advanced
Effect timing
Four hooks that all run 'after render', at four different moments.
08On this page
The order, precisely
- React renders — your components run, producing the next tree.
- Commit: the DOM is mutated. Refs are attached.
useLayoutEffectcleanups run, then its effects. Synchronously.- React yields; the browser paints.
useEffectcleanups run, then its effects.
Within each phase, children run before parents — a child's effect fires before its parent's, because a parent may depend on its children having set themselves up.
| Hook | Runs | Blocks paint | For |
|---|---|---|---|
useEffect | after paint | no | almost everything |
useLayoutEffect | after DOM, before paint | yes | measuring and adjusting |
useInsertionEffect | before DOM mutations | yes | injecting styles — CSS-in-JS only |
| Ref callbacks | during commit | yes | reacting to a node existing |
See where your code lands
When useLayoutEffect is the right answer
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipTop(anchorTop - height); // before the user sees it
}, [content]);
With useEffect, the tooltip paints in the wrong place, then jumps.
With useLayoutEffect, React blocks the paint, applies the
correction, and the browser draws once — the intermediate state never
reaches the screen.
The rule: if the user would see a flicker, it is a layout effect. Measuring, scroll restoration, positioning a popover. Everything else — fetching, subscriptions, logging, timers — is a plain effect, because blocking paint for them is pure cost.
It warns during server rendering
useLayoutEffect cannot run on the server, so React warns when a
component using it is server-rendered. Either the work belongs in
useEffect, or the component should not render on the server.
The common workaround — aliasing to useEffect on the
server — hides the warning without fixing the flicker.
Cleanup timing
useEffect(() => {
connect(roomId);
return () => disconnect(roomId);
}, [roomId]);
Change roomId from A to B and the order is: disconnect(A),
then connect(B). The cleanup closes over the render that created it, so it
disconnects the room it actually connected to — not the new one. That is
why a cleanup reading current state rather than its own captured values is a
bug.
Effects and refs
function Chart() {
const ref = useRef(null);
useEffect(() => {
const chart = new ChartLib(ref.current); // ✓ node exists by now
return () => chart.destroy();
}, []);
return <div ref={ref} />;
}
Refs are attached during commit, so any effect can rely on
ref.current. During render it is null, which is why
DOM work never belongs in the component body.
The order that surprises people
Parent renders
Child renders
Child effect runs // ← child first
Parent effect runs
A parent effect that measures a child can rely on the child having mounted and laid out. The reverse is not true: a child effect cannot assume its parent has finished setting up.
Strict Mode's double invocation
In development React mounts, runs effects, runs cleanups, and runs the effects again. Anything that is not idempotent shows immediately — two socket connections, a doubled analytics event, two intervals. It is a detector, not a bug, and the same failure would appear in production the first time a user navigated away and back.
Until React 19.3, a root that was hydrated from server HTML skipped this double run, while a client-rendered root did it — so an effect bug could hide in an SSR app and show in a client-only one. Since 19.3 Strict Mode double invokes effects during hydration too. An upgrade can therefore surface a doubled socket or analytics call that was always there.
Escaping the timing entirely
Effect-ordering questions are often a sign that the effect should not exist.
Before reaching for useLayoutEffect, check whether the value can
be derived during render, whether a key would reset the thing you
are trying to synchronise, or whether CSS can do the positioning. The best fix
for a timing problem is usually to remove the timing.
Interview answer, one sentence
"useLayoutEffect runs after the DOM is updated but before paint,
so it is for measuring and correcting layout where a plain effect would
flicker; everything else belongs in useEffect, which runs after
paint and does not block it. Within either phase children run before parents,
and a cleanup sees the values of the render that created it."
Opens in the editor: write it, run it, and check it against real tests.
All 54 for this level →Finished reading?
0 of 57 chapters read in this series