Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
useLayoutEffect fires synchronously after React mutates the DOM but before the browser paints. This makes it the correct tool when you need to read a DOM measurement (getBoundingClientRect, scrollHeight) and immediately apply a visual adjustment — doing the same read in useEffect would show a frame where the adjustment hasn't been applied yet, causing a visible flash or layout jump. The cost is real: synchronous work in useLayoutEffect is on the critical rendering path. Reserve it strictly for DOM measurement-then-adjustment flows; useEffect handles everything else.
useLayoutEffect runs synchronously after DOM mutations, before paint. Use it when you need to read layout and apply a style change without the user seeing an intermediate state.
import { useRef, useState, useLayoutEffect, useEffect } from 'react';
// useLayoutEffect: tooltip positions itself BEFORE paint — no flash
export function Tooltip({ text }: { text: string }) {
const ref = useRef<HTMLDivElement>(null);
const [style, setStyle] = useState<React.CSSProperties>({});
useLayoutEffect(() => {
const el = ref.current;
if (!el) return;
const { right } = el.getBoundingClientRect();
if (right > window.innerWidth) {
// nudge left before the browser has a chance to paint
setStyle({ transform: 'translateX(-100%)' });
}
}, [text]);
return (
<div ref={ref} style={{ position: 'absolute', ...style }}>
{text}
</div>
);
}
// useEffect: same code but causes a visible flicker
export function FlickyTooltip({ text }: { text: string }) {
const ref = useRef<HTMLDivElement>(null);
const [style, setStyle] = useState<React.CSSProperties>({});
useEffect(() => {
const el = ref.current;
if (!el) return;
const { right } = el.getBoundingClientRect();
if (right > window.innerWidth) {
setStyle({ transform: 'translateX(-100%)' });
// setState triggers a second render AFTER the first painted frame — flash!
}
}, [text]);
return <div ref={ref} style={{ position: 'absolute', ...style }}>{text}</div>;
}console.time('layout') at the start and console.timeEnd('layout') at the end. Do the same in a useEffect version. Compare the times — both should be similar, but the layout version blocks the paint thread.while (Date.now() < Date.now() + 50) {} (50ms busy loop) inside useLayoutEffect. Notice the entire frame is delayed — the browser won't paint until useLayoutEffect completes. Do the same in useEffect and observe the component renders first, then the effect fires.Use these three in order. Each builds on the one before.
In one paragraph, explain when to use useLayoutEffect instead of useEffect. What would go wrong with useEffect in the tooltip repositioning example?
Walk me through the React commit phase. In what order do useLayoutEffect, DOM mutations, browser paint, and useEffect fire? Why does useLayoutEffect's synchronous execution prevent the flash that useEffect causes?
useLayoutEffect on the server throws a warning because there is no DOM to read. How do libraries like Radix UI and Floating UI handle this — and what's the pattern for writing a hook that uses useLayoutEffect on the client without crashing on the server?