Good to knowAdvanced React APIs · 04

useLayoutEffect

Measure before the browser paints: the recorded frame where a useEffect tooltip covers its button.

On this page · 7 sections
  1. In one minute
  2. The example: the shipping tooltip
  3. How it works
  4. Common mistakes
  5. Interview Q&A
  6. Related
  7. Sources

In one minute

useLayoutEffect(setup, deps) has the same signature as useEffect, but different timing. It runs during the commit, after the DOM is updated and before the browser can paint, and a state update made inside it is rendered synchronously, also before paint. Use it when an effect measures the DOM and changes what’s shown (position, size, scroll), so the user never sees the unmeasured version. Default to useEffect; layout effects block painting.

You’ll be able to: spot the measure-then-adjust flicker, fix it with useLayoutEffect, and explain why it’s not the default.

useEffect
Tooltip.tsx
1function Tooltip({ anchor }) {2  const ref = useRef(null)3  const [height, setHeight] = useState(0) // unknown at first4  useEffect(() => {5    setHeight(ref.current.getBoundingClientRect().height)6  }, [])7  const fitsAbove = anchor.top - height >= 08  const top = fitsAbove ? anchor.top - height : anchor.bottom9  return <div ref={ref} style={{ top }}>Free shipping…</div>10}
What you see
Tooltip: not shown
Console (recorded)
render: height 0 → above
frame 1 painted: covering the button
effect: measured 58px
render: height 58 → below
frame 2 painted: below the button
  1. render phaseconst [height, setHeight] = useState(0) // unknown at firstThe height isn’t known yet (0), so the tooltip is placed “above” the button at a height of 0: right on top of the button.
  2. browserReact commits, and useEffect hasn’t run yet, so the browser paints that frame. The user sees the tooltip covering the button.
  3. effectssetHeight(ref.current.getBoundingClientRect().height)Now the effect measures 58px; there’s no room above, so it moves below.
  4. browserThe next frame is correct. The tooltip visibly jumped.

0 / 0
Hovering “Shipping ⓘ” shows a tooltip that measures itself to decide whether it fits above the button. Frames were read in requestAnimationFrame, right before each paint (recorded).

The example: the shipping tooltip

Next to the price there’s a “Shipping ⓘ” button. Hovering it for 200 ms shows a tooltip. The tooltip prefers to sit above the button, but if there isn’t room it goes below. It can’t know whether it fits until it knows its own height, and it can’t know its height until it’s in the DOM.

function Tooltip({ anchor, useMeasureEffect }: { anchor: Rect; useMeasureEffect: typeof useLayoutEffect }) {
	const ref = useRef<HTMLDivElement>(null)
	const [height, setHeight] = useState(0) // unknown until it's in the DOM

	useMeasureEffect(() => {
		const measured = ref.current!.getBoundingClientRect().height
		log(`  effect: measured ${measured}px`)
		setHeight(measured)
	}, [])

	const fitsAbove = anchor.top - height >= 0
	const top = fitsAbove ? anchor.top - height : anchor.bottom
	log(`  render: height ${height} → ${fitsAbove ? 'above' : 'below'}`)
	return (
		<div ref={ref} className="tooltip" style={{ top, left: anchor.left }}>
			Free shipping on orders over $50. Returns are free for 30 days.
		</div>
	)
}

useMeasureEffect is useEffect or useLayoutEffect. The button is near the top of the page and the tooltip is 58px tall, so the right answer is “below”.

To see what the user sees, the lesson logs what’s on screen in requestAnimationFrame, which the browser runs right before each paint. Each hover was recorded 20 times.

With useEffect

The most common sequence:

  render: height 0 → above
frame 1 painted: covering the button
  effect: measured 58px
  render: height 58 → below
frame 2 painted: below the button
19 of 20 hovers painted the tooltip covering the button

The browser painted a frame between the commit and the effect. For one frame, the tooltip sat on top of the button, then jumped below it.

With useLayoutEffect

  render: height 0 → above
  effect: measured 58px
  render: height 58 → below
frame 1 painted: below the button
0 of 20 hovers painted the tooltip covering the button

The measurement and the second render both happened before the first frame, so the first thing painted was the correct position.

How it works

Common mistakes

Interview Q&A

What's the difference between useEffect and useLayoutEffect?

Same signature, different timing. useLayoutEffect runs during the commit, after DOM updates and before the browser paints, and its state updates render synchronously. useEffect runs after the commit and can run after paint.

When does useEffect cause a visible bug here?

When it measures the DOM and then moves or resizes something. The browser may paint the unmeasured version first. Recorded: with useEffect, most hovers painted the tooltip covering the button for one frame. With useLayoutEffect, none did.

Why not always use useLayoutEffect?

It blocks painting until it’s done, and so do its updates. For effects that don’t change what’s on screen (subscriptions, logging, fetching), that’s delay for nothing.

Give real uses.

Positioning a tooltip or popover based on available space; computing how many cards fit a measured container; keeping a chat scrolled to the bottom when messages arrive; measuring text to truncate it.

How did the recording know what was painted?

requestAnimationFrame callbacks run right before each paint, so the tooltip’s position read there is what that frame shows.

Sources

Primary source: react.dev/reference/react/useLayoutEffect