useEffect runs after the browser paints, which can cause a visible flicker for layout-dependent updates like repositioning a tooltip. useLayoutEffect fixes this by running synchronously beforehand, at the cost of potentially blocking rendering. This lesson covers when that tradeoff is worth making.
1The Flicker Problem
Measuring a DOM element and using that measurement to reposition another element inside useEffect can cause a visible flicker: the browser has already painted the incorrect position by the time the effect runs and corrects it, so the user briefly sees the wrong state before it snaps to the right one.
2useLayoutEffect Runs Before Paint
useLayoutEffect shares the exact same API as useEffect, but React runs it synchronously after DOM mutations are committed and before the browser paints. Any DOM reads or writes performed inside it are invisible to the user, who only ever sees the final, corrected result.
3The Cost: Blocking the Browser
That synchronous timing is also useLayoutEffect's tradeoff: because it blocks the browser from painting until it completes, slow code inside it delays every visual update on the page. useEffect, by contrast, runs asynchronously after paint and never blocks rendering.
4The Default Rule: Reach for useEffect First
Given the blocking cost, useEffect should remain the default choice for nearly all side effects — data fetching, subscriptions, logging, timers. useLayoutEffect is reserved for the narrow case of needing to read layout information and synchronously apply a DOM-visible correction before the next paint.
5A Real Example: Auto-Scrolling a Chat Window
A chat window that must be scrolled to its newest message before the user sees it is a textbook use case: reading scrollHeight and setting scrollTop inside useEffect would let the browser paint the unscrolled list for a frame first, producing a visible jump that useLayoutEffect avoids.
6Step-by-Step Breakdown
The Flicker Problem. You measure a DOM element's height inside useEffect and use it to reposition a tooltip. The browser has already painted the tooltip in the wrong spot by the time your effect runs — for one frame, the user sees it jump. This visible flicker is exactly the problem useLayoutEffect exists to solve.
useLayoutEffect Runs Before Paint. useLayoutEffect has the exact same API as useEffect, but React runs it synchronously after DOM mutations are committed and BEFORE the browser paints the screen. Any DOM reads or writes you do inside it are invisible to the user — they see only the final, correct result.
Why does useLayoutEffect avoid the visible flicker that useEffect can cause when repositioning an element?
- →It runs synchronously before the browser paints the updated DOM
- →It simply executes JavaScript faster than useEffect
The Cost: Blocking the Browser. That synchronous timing is also the tradeoff. Because useLayoutEffect blocks the browser from painting until it finishes, slow code inside it delays every visual update, potentially making the whole page feel janky. useEffect, by contrast, runs asynchronously after paint and never blocks rendering.
The Default Rule: Reach for useEffect First. Because of that blocking cost, useEffect should be your default for nearly everything: data fetching, subscriptions, logging, timers. Reach for useLayoutEffect only for the narrow case where you must read layout (like an element's size or position) and synchronously write a DOM-visible change before the user's next paint.
Which hook should be your default choice for a data-fetching effect that doesn't touch layout?
- →useEffect
- →useLayoutEffect
A Real Example: Auto-Scrolling a Chat Window. A chat window that must scroll to the newest message before the user sees anything is a textbook useLayoutEffect case: reading scrollHeight and setting scrollTop inside useEffect would let the browser paint the un-scrolled list for one frame first, causing a visible jump.
Mastery Achieved. You now understand useLayoutEffect: it runs synchronously before the browser paints, which eliminates layout-related flicker at the cost of potentially blocking rendering if the work is slow. useEffect stays the correct default for almost everything else. Next, you'll learn useImperativeHandle for exposing a controlled, custom API through a ref.
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Avoid Layout Thrash That Delays Focus or Screen Reader Announcements
Slow useLayoutEffect code blocks the browser's paint, which can also delay when assistive technology perceives a UI update — keep layout-effect logic fast and minimal, especially around focus management.
SEO Implications
- 1
useLayoutEffect Has No Effect in Server Rendering
It only runs in the browser after hydration, identically to useEffect in that respect — neither contributes to the server-rendered HTML a crawler initially sees, so layout-dependent content shouldn't be relied upon for SEO-critical text.
Best Practices
Default to useEffect Unless You Have a Specific Layout Reason Not To
Only reach for useLayoutEffect when a visible flicker from a layout-dependent DOM read/write has actually been observed or is clearly unavoidable — using it by default adds unnecessary paint-blocking risk.
Keep useLayoutEffect Bodies as Fast as Possible
Since it blocks painting until it completes, avoid expensive computation, network calls, or anything not directly related to a synchronous layout measurement/correction inside a useLayoutEffect callback.
Frequent Bugs
The whole page feels janky and unresponsive after adding a useLayoutEffect for a data-fetching call.
useLayoutEffect blocks the browser's paint until it finishes running — it should never be used for asynchronous work like data fetching, which has no reason to block rendering. Move it to a regular useEffect.
A tooltip flickers in the wrong position for a moment before snapping into place.
The measurement and repositioning logic is running inside useEffect, which fires after the browser has already painted. Move the measurement and DOM write into useLayoutEffect so it happens before the paint the user sees.
Real-World Examples
Positioning a Tooltip Based on Its Own Measured Size
A tooltip component needs to flip above its trigger element if there isn't enough space below it, which requires measuring the tooltip's rendered height first. Doing that measurement and the resulting position update inside useLayoutEffect ensures the user only ever sees the tooltip in its final, correct position, never a flicker of the wrong one.
function Tooltip({ triggerRef, children }) {
const tooltipRef = useRef(null);
const [top, setTop] = useState(0);
useLayoutEffect(() => {
const { height } = tooltipRef.current.getBoundingClientRect();
const triggerRect = triggerRef.current.getBoundingClientRect();
setTop(triggerRect.top - height - 8);
}, []);
return <div ref={tooltipRef} style={{ top }}>{children}</div>;
}