useEffect and useLayoutEffect look almost identical on the surface — same signature, same dependency array, same cleanup function pattern. Copy-paste one into the other and your code will often still work. That similarity is exactly why they get confused so often, and why picking the wrong one can quietly introduce visual bugs that are hard to trace back to their source.
The difference isn’t about what they do — it’s about when they run relative to the browser painting the screen.
The Core Difference: Timing
Both hooks let you run side effects after a component renders. The distinction is in the scheduling:
- useEffect runs asynchronously, after the browser has painted the screen. React finishes rendering, the browser draws the updated UI, and only then does your effect run.
- useLayoutEffect runs synchronously, after React has made its DOM changes but before the browser paints. The browser waits for your effect to finish before it shows anything to the user.
useEffect(() => {
console.log('Runs after paint');
}, []);
useLayoutEffect(() => {
console.log('Runs before paint, blocking it');
}, []);
This timing difference sounds subtle, but it has real consequences for what the user actually sees.
Why the Timing Matters
Picture a component that measures its own size after mounting, then repositions itself based on that measurement — a tooltip, for example, that needs to flip position if it would otherwise render off-screen.
If you do this measurement-and-reposition logic in useEffect, here’s what happens: React renders the tooltip in its default position, the browser paints it, the user sees it flash in the wrong place for a frame, and then your effect runs, measures the DOM, and repositions it. That flash is a real, visible flicker.
If you do the same logic in useLayoutEffect, React renders the tooltip, your effect runs and repositions it before the browser paints anything, so the user only ever sees the corrected, final position. No flicker.
function Tooltip({ targetRef }) {
const [position, setPosition] = useState({ top: 0, left: 0 });
const tooltipRef = useRef(null);
useLayoutEffect(() => {
const rect = tooltipRef.current.getBoundingClientRect();
const targetRect = targetRef.current.getBoundingClientRect();
// Measure and adjust position before the browser paints
setPosition(calculatePosition(rect, targetRect));
}, [targetRef]);
return <div ref={tooltipRef} style={position}>Tooltip content</div>;
}
This is the textbook case for useLayoutEffect: anything that reads layout (getBoundingClientRect, scroll position, computed styles) and then synchronously writes a DOM mutation that affects what the user sees.
Why useEffect Is Still the Default
Given that useLayoutEffect prevents visual flicker, it’s tempting to assume it’s strictly better. It isn’t — the synchronous, blocking nature that fixes the flicker problem is also a performance cost.
Because useLayoutEffect blocks the browser from painting until it finishes, an expensive computation inside it will delay the user seeing anything — not just the part your effect touches. useEffect, by contrast, never blocks the paint. The screen updates, and your side effect happens afterward, off the critical rendering path.
For the vast majority of effects — fetching data, setting up subscriptions, logging, manually syncing state with an external system — there’s no visual output to get right on the first paint, so there’s nothing for useLayoutEffect to protect you from. Using it anyway just adds unnecessary blocking.
// This has no reason to block paint — useEffect is correct here
useEffect(() => {
const subscription = dataSource.subscribe(handleChange);
return () => subscription.unsubscribe();
}, []);
This is why React’s own documentation and most style guides treat useEffect as the default, and useLayoutEffect as the exception you reach for only when you have a specific, visible flicker to solve.

A Simple Decision Rule
Ask one question: does this effect read the DOM’s layout and then synchronously change something the user will see?
- If yes — measuring an element, then repositioning or resizing something based on that measurement — use
useLayoutEffect. - If no — almost everything else, including data fetching, event listeners, timers, analytics, and syncing with external stores — use
useEffect.
A useful mental shortcut: useLayoutEffect is for effects that would otherwise cause a visible “jump” or “flash” if delayed until after paint. If nothing on screen would look wrong for a frame, you don’t need it.
Server-Side Rendering Caveat
One more practical difference: useLayoutEffect doesn’t run during server-side rendering, and React will log a warning if you use it in a component that renders on the server, because there’s no browser DOM to measure yet. If you’re writing an SSR-compatible component that needs layout-based logic, you typically guard it or fall back to useEffect for the server render and let the client take over.
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
This pattern is common enough that it shows up in several popular libraries (React Redux uses a version of it internally) — it lets you get useLayoutEffect‘s behavior in the browser without breaking server rendering.
Quick Reference
useEffect |
useLayoutEffect |
|
|---|---|---|
| Runs | After the browser paints | Before the browser paints |
| Blocks rendering | No | Yes |
| Best for | Data fetching, subscriptions, logging, general side effects | Layout measurement + synchronous DOM adjustments |
| Risk if misused | Visible flicker (if it should’ve been layout-based) | Unnecessary blocking, slower perceived performance |
| Works in SSR | Yes | No — logs a warning, needs a fallback |
The Takeaway
useEffect and useLayoutEffect aren’t two ways of doing the same thing — they’re a trade-off between speed and visual correctness. useEffect keeps rendering fast by never blocking the paint, which is right for almost everything. useLayoutEffect trades a bit of that speed to guarantee the user never sees an intermediate, incorrect frame — which matters only when your effect is actually adjusting something visual based on a DOM measurement. Default to useEffect, and reach for useLayoutEffect only when you can point to the specific flicker it’s fixing.