“What is the difference between useEffect and useLayoutEffect, and when have you actually needed the second one?” Interviewers want a concrete story, and the classic one is a tooltip that appears in the wrong place for a frame and then jumps. Explaining why that flicker happens, and why useLayoutEffect removes it, shows you understand the order of rendering, committing and painting.
The second half of the question is about restraint: useLayoutEffect blocks the browser, so using it everywhere makes an app feel slower, not faster.
Before you start
You should be comfortable with useEffect, refs to DOM nodes, and getBoundingClientRect(). A rough model of the browser pipeline helps: JavaScript changes the DOM, the browser calculates layout, then it paints pixels. The behaviour below is the same in React 18 and 19.
The short answer
Both hooks run after React has updated the DOM. useLayoutEffect runs synchronously before the browser paints, while useEffect usually runs after paint. If you need to measure an element and then change layout based on that measurement, such as placing a tooltip above or below its anchor, do it in useLayoutEffect: any state you set there triggers a synchronous re-render, so the user only ever sees the final position. Use useEffect for everything else, because layout effects delay painting. Layout effects do not run on the server.
How it works
The commit sequence for one update looks like this:
render (call components) -> commit DOM changes
-> run useLayoutEffect cleanups and setups (synchronous)
-> browser calculates layout and paints
-> run useEffect cleanups and setupsIf a layout effect calls setState, React processes that update immediately and commits again, still before paint. The browser paints only the second result. If an ordinary effect calls setState, the first result has usually already been painted, so the user sees an intermediate frame.
Reading layout properties like getBoundingClientRect() or offsetHeight forces the browser to calculate layout synchronously. That is fine once per update; doing it in many components at once, or alternating reads and writes, causes layout thrashing.
Step-by-step walkthrough
Step 1: Render a first guess
The tooltip renders with a default position and an unknown height:
function Tooltip({ anchorRect, children }) {
const ref = useRef(null);
const [height, setHeight] = useState(0);
const fitsAbove = anchorRect.top - height > 0;
const top = fitsAbove ? anchorRect.top - height : anchorRect.bottom;
return (
<div ref={ref} className="tooltip" style={{ position: 'fixed', left: anchorRect.left, top }}>
{children}
</div>
);
}On the first pass height is 0, so the tooltip is placed at the anchor’s top edge, overlapping it.
Step 2: Measure before paint
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setHeight(height);
}, [children]);React commits the first guess to the DOM, the layout effect measures the real height, sets state, and React re-renders synchronously with the correct position. Only then does the browser paint. With useEffect, users would see the tooltip overlap the anchor for one frame and then jump.
Step 3: Keep the measurement narrow
Measure only what you need and only when its inputs change. Here the content (children) determines the height. If the anchor moves on scroll or resize, subscribe to those events (or use a ResizeObserver) and update the anchor rectangle, rather than measuring on every render with no dependency array.
Step 4: Handle server rendering
During server rendering there is no DOM, so layout effects do not run, and the server HTML contains the first guess. Options: render the tooltip only on the client (tooltips usually appear after interaction anyway), hide it with visibility: hidden until measured, or choose a first guess that is acceptable when JavaScript has not run. React 18 used to warn about useLayoutEffect during server rendering; React 19 no longer emits that warning, but the effect still does not run on the server.
Worked scenario
An autocomplete dropdown should open below the input, or above it when there is not enough room at the bottom of the viewport. The first implementation measured in useEffect:
useEffect(() => {
const rect = menuRef.current.getBoundingClientRect();
setPlaceAbove(rect.bottom > window.innerHeight);
}, [options]);Near the bottom of the screen, users saw the menu flash below the input, partly off-screen, then snap above it. On slower phones the flash lasted long enough to look broken, and screen recordings in bug reports showed it clearly.
Switching to useLayoutEffect removed the flash, because the placement correction happened before the first paint. The team then noticed the effect re-measured on every keystroke because options was a new array each time; memoising the options list reduced the work to when results actually changed.
A pure CSS solution is worth mentioning too: modern CSS anchor positioning with fallback positions can flip menus without JavaScript in browsers that support it. In an interview, mentioning that you would check whether CSS can solve the problem first is a strong signal.
Common mistake
- “
useLayoutEffectis faster.” It runs earlier, but it blocks painting. Overusing it makes the app feel slower. - Using it for data fetching or subscriptions. Nothing visual depends on those finishing before paint; use
useEffect. - Measuring in render. The DOM for this render does not exist yet during render, and refs are not attached.
- Measuring every render without dependencies. Each forced layout calculation costs time; scope it to the inputs that change the size.
- Assuming it runs on the server. It does not, so the server output must be acceptable on its own.
Verify the behavior
Log the order of execution:
function Order() {
console.log('render');
useLayoutEffect(() => console.log('layout effect'));
useEffect(() => console.log('effect'));
return <div>check the console</div>;
}You will see render, layout effect, effect. To see the flicker difference, throttle the CPU to 6x slowdown in Chrome DevTools’ Performance panel, record while opening the tooltip, and step through the frames. With useEffect, one painted frame shows the wrong position; with useLayoutEffect, none do.
Follow-up questions
Is useInsertionEffect related? It runs even earlier, before layout effects, and is meant for CSS-in-JS libraries to inject styles. Application code rarely needs it.
Can I avoid the effect entirely with a ref callback? A ref callback runs when the node attaches, also before paint, and can measure there. In React 19 ref callbacks can return a cleanup, which makes them good for attaching a ResizeObserver to one element.
What does flushSync have to do with this? It forces state updates to commit synchronously, which is useful when you must measure or scroll right after a state change inside an event handler.
Does useEffect ever run before paint? For updates caused by discrete user input like clicks, React may flush passive effects before the browser paints, but you should not rely on that for visual correctness.
Interview exercise
A component renders a long message and should show a “Read more” button only when the text overflows three lines. The current code checks el.scrollHeight > el.clientHeight in useEffect and sets isOverflowing. Users see the button pop in after the text appears. What would you change and what else should you consider?
Answer and reasoning
The check affects what is painted, so it belongs in useLayoutEffect: the measurement and the resulting re-render then happen before paint, and the button is present in the first visible frame. The dependency should be the text (and anything affecting width). Because the container can change width when the window resizes, a ResizeObserver on the element should re-run the check; React 19’s ref callback with cleanup is a tidy place for it. For server rendering, decide on a default, such as showing the clamped text without the button, so the server HTML is reasonable before hydration. Finally, measure once per change, not on every render, to avoid forcing layout repeatedly.
Continue learning
Practise with the React interview questions and the React MCQs. Related notes: effect cleanup, useRef and imperative handles and hydration consistency. Official reference: the useLayoutEffect reference on react.dev.