Difficulty: Intermediate
What is the difference between useLayoutEffect and useEffect? When should you use each?
Alright, so useLayoutEffect and useEffect look almost identical in code, but the timing difference between them is everything. Let me walk you through exactly what happens under the hood.
Think of the browser rendering process like a factory assembly line. When React finishes computing what the UI should look like, it writes those changes to the DOM. That is step one. Now, after the DOM is updated but BEFORE the browser actually paints those pixels on your screen, useLayoutEffect fires. It runs synchronously, meaning the browser literally cannot paint until your useLayoutEffect code finishes running. Then, once the paint happens and the user sees the updated screen, useEffect fires asynchronously in the background.
Here is the exact sequence, step by step: 1. React calculates the new UI (the render phase). 2. React commits the changes to the DOM (the commit phase). 3. useLayoutEffect runs RIGHT HERE, synchronously. The browser is blocked and cannot paint yet. 4. The browser paints the screen. The user sees the update. 5. useEffect runs asynchronously, after the paint.
So why does this matter in practice? Imagine you are building a tooltip component. When the tooltip first appears, you need to measure where the anchor element is on the screen, then position the tooltip relative to it. If you use useEffect for this, here is what happens: React renders the tooltip at position (0, 0) or some default, the browser paints it there for a brief moment (the user might see it flash in the wrong spot), then useEffect fires, you measure the DOM, update the position, and React re-renders the tooltip in the correct spot. That flash of the tooltip in the wrong position is called a visual flicker, and it looks janky.
With useLayoutEffect, the measurement and repositioning happens BEFORE the browser paints, so the user never sees the tooltip in the wrong position. It just appears in the right place from the start.
Now, here is the important practical advice: use useEffect for almost everything. Seriously, like 95% of your effects should be useEffect. Data fetching, subscriptions, logging, analytics, timers -- all useEffect. Only reach for useLayoutEffect when you specifically need to read something from the DOM layout (like element dimensions or scroll position) and then immediately make a visual change based on that measurement. Tooltips, popovers, dropdown menus that need to flip direction, auto-scrolling, and animations that depend on element positions are the classic use cases.
One gotcha that trips people up in real projects: useLayoutEffect causes a warning during server-side rendering (SSR) with frameworks like Next.js. The reason is simple -- there is no DOM on the server, so a synchronous DOM measurement makes no sense. The fix is usually to use useEffect with a mounted state flag, or to use the useIsomorphicLayoutEffect pattern where you pick useLayoutEffect on the client and useEffect on the server.
Also worth knowing: React 18 introduced useInsertionEffect, which runs even BEFORE useLayoutEffect. It is designed specifically for CSS-in-JS libraries like styled-components or Emotion that need to inject style tags before any layout measurements happen. You will probably never use useInsertionEffect directly, but knowing it exists shows you understand the full effect timing hierarchy.
A common mistake junior developers make is overusing useLayoutEffect because they heard it runs first and assume first equals better. But because useLayoutEffect is synchronous and blocks painting, heavy computation inside it will literally freeze your UI. The browser cannot show anything new until your useLayoutEffect finishes. So if you put an expensive calculation in there, your users stare at a frozen screen. Only use it when you genuinely need that pre-paint timing.
import { useLayoutEffect, useState, useRef } from 'react';
function Tooltip({ text, anchorRef }) {
const tooltipRef = useRef(null);
const [position, setPosition] = useState({ top: 0, left: 0 });
// Must use useLayoutEffect to measure before paint
// useEffect would cause a flicker - briefly wrong position visible
useLayoutEffect(() => {
const anchor = anchorRef.current;
const tooltip = tooltipRef.current;
if (!anchor || !tooltip) return;
const anchorRect = anchor.getBoundingClientRect();
const tooltipRect = tooltip.getBoundingClientRect();
setPosition({
top: anchorRect.top - tooltipRect.height - 8,
left: anchorRect.left + (anchorRect.width - tooltipRect.width) / 2
});
});
return (
<div
ref={tooltipRef}
style={{
position: 'fixed',
top: position.top,
left: position.left
}}
>
{text}
</div>
);
}
// useEffect timing (illustrative)
function Order() {
useLayoutEffect(() => console.log('1. useLayoutEffect'));
useEffect(() => console.log('3. useEffect'));
return <div>Component</div>;
// console.log('2. paint') happens between them
}
useLayoutEffect fires synchronously before paint. For tooltip positioning, using useEffect would briefly show the tooltip in the wrong position.
useLayoutEffect, useEffect, Paint, DOM, Timing