useEffect vs useLayoutEffect
By FrontendPro Editorial Team Updated 9/26/2026
Answer
Both run after the DOM has been updated. The difference is when relative to the paint: useLayoutEffect runs before the browser paints and blocks it; useEffect runs after, asynchronously.
render -> DOM updated -> useLayoutEffect -> browser paints -> useEffect
^ blocking ^ not blocking
Which means
useEffect is the default. Data fetching, subscriptions, logging, timers, anything the user does not see — all of it belongs here, because blocking the paint for it only makes the page slower.
useLayoutEffect is for the narrow case where you must measure the DOM and change it again before anyone sees the intermediate state:
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setPosition(height > space ? 'above' : 'below'); // tooltip flips
}, [open]);
With useEffect here the tooltip would paint in the wrong place and then jump. That visible flicker is the whole reason the second hook exists.
The legitimate use cases
- Measuring an element to position something relative to it — tooltips, popovers, dropdowns.
- Reading or restoring scroll position before paint.
- Synchronously fixing up a layout you cannot express in CSS.
- Setting focus, when doing it after paint would flash.
Anything else, use useEffect.
The cost
useLayoutEffect runs on the main thread between DOM update and paint, so slow work in it delays the frame directly. A getBoundingClientRect forces a synchronous layout. Doing this in a list of a hundred items is a stutter you can feel.
The SSR warning
useLayoutEffect does nothing during server rendering and React warns about it, because there is no layout to measure. The usual workaround:
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
But if a component genuinely needs to measure before paint, it is a client component — the better fix is often to render it only on the client rather than silence the warning.
One thing that is identical
The cleanup rules, the dependency array, and the double-invocation under StrictMode are the same for both. So a missing cleanup is a bug in either one; switching hooks never fixes it.
