useEffect vs useLayoutEffect: same signature, different timing
Both hooks share the signature useXxxEffect(setup, deps). The
ONLY difference is when the setup function runs
relative to the browser paint. useEffect is asynchronous — the
browser paints first, THEN the effect runs, so the user may briefly see a
stale DOM. useLayoutEffect is synchronous — it runs after DOM
mutation but BEFORE paint, so any setState inside it produces
a re-render that the user sees as one flicker-free frame.
| phase | what happens | which hook runs |
|---|---|---|
| 1. render | React calls your component, computes the virtual DOM | — |
| 2. commit | React mutates the real DOM to match the virtual DOM | — |
| 3. layout | useLayoutEffect setup runs synchronously — can read layout (offsetWidth, getBoundingClientRect), can setState |
useLayoutEffect |
| 4. paint | browser paints pixels to the screen — user sees the frame | — |
| 5. passive | useEffect setup runs asynchronously — non-blocking, the user already saw the painted DOM |
useEffect |
1 · the measure-before-paint hook (edit me)
2 · Babel compiles JSX → element tree
<LayoutEffectDemo/> becomes
React.createElement(LayoutEffectDemo). After commit,
React fires useLayoutEffect synchronously — it reads
boxRef.current.offsetWidth, calls setWidth,
and React re-renders BEFORE the browser paints.
// (hit "compile & render" to see Babel's output)
3 · live React (measure-before-paint, proven)
Click Grow +50 / Shrink −50:
the Measured: W×Hpx line updates in the SAME frame as the box resize.
The gold-check asserts the sync pipeline:
initial 100×100 → Grow 150×150 → Grow 200×200 → Shrink 150×150.
If we used useEffect instead, you would see one painted frame
with a stale measurement before it corrected.
dimensions: — · gold: 100×100 → Grow → 150×150 → Grow → 200×200 → Shrink → 150×150
intent → hook
| intent | hook | why |
|---|---|---|
| data fetching, subscriptions, logging | useEffect |
non-blocking; user shouldn't wait on a network round-trip before paint |
| measure element size/position before showing it | useLayoutEffect |
reads offsetWidth/getBoundingClientRect before paint — no flicker |
| scroll to a position right after content mounts | useLayoutEffect |
sets scrollTop before the user sees the wrong scroll offset |
| position a tooltip/popover relative to an anchor | useLayoutEffect |
measures the anchor box, computes coords, paints once — no jump |
animate from 0 to a measured value |
useLayoutEffect + useState |
read the target size, then animate; otherwise the first frame is wrong |
| SSR (no real DOM) | useEffect or useInsertionEffect |
useLayoutEffect warns on the server — wrap in typeof window !== 'undefined' or use a useIsomorphicLayoutEffect shim |