Skip to main content

What is the difference between useEffect and useLayoutEffect?

useEffect vs useLayoutEffect - the only difference is timing: one runs after the browser paints, the other blocks paint until it finishes.

Theory

TL;DR

  • useEffect fires after the browser paints the screen (async, post-paint)
  • useLayoutEffect fires before paint, blocking the browser until the callback finishes (sync, pre-paint)
  • Analogy: useEffect is the cleanup crew arriving after guests already saw the mess; useLayoutEffect cleans up before anyone walks in
  • Need DOM measurements or a no-flicker correction? Use useLayoutEffect. Everything else goes to useEffect
  • useLayoutEffect warns in SSR (Node.js); useEffect does not

Quick example

jsx
function Box() { const [count, setCount] = useState(0); const ref = useRef(); // Runs AFTER paint - user sees old width first, then it jumps useEffect(() => { ref.current.style.width = `${count * 20}px`; }, [count]); // Swap to this - width is set before paint, no flicker // useLayoutEffect(() => { // ref.current.style.width = `${count * 20}px`; // }, [count]); return ( <div ref={ref} style={{ background: '#eee' }} onClick={() => setCount(c => c + 1)}> Click: {count} </div> ); }

With useEffect, the browser paints the old width first, then the effect fires and the box jumps. Swap to useLayoutEffect and the width is corrected before a single pixel is drawn.

Key difference

Both hooks accept the same signature, and cleanup works identically. The only real difference is when they run inside React's commit phase. useLayoutEffect fires synchronously after DOM mutations but before the browser paints. useEffect is scheduled post-paint at a lower priority. That gap is milliseconds small, but visible to the eye whenever DOM measurements or style corrections are involved. I have seen this exact issue in production where a tooltip positioned with useEffect would visibly snap into place on every render.

When to use

  • DOM measurements (width, height, scroll position) before render → useLayoutEffect
  • Correcting layout to prevent a visible jump or flicker → useLayoutEffect
  • Animations that read layout before writing values → useLayoutEffect
  • Data fetching, timers, event listeners, subscriptions → useEffect
  • Anything that does not touch visible DOM layout → useEffect

Comparison table

AspectuseEffectuseLayoutEffect
TimingAfter paint (async)Before paint (sync)
Blocks browser paint?NoYes
DOM layout readsCauses flickerSafe, matches visible state
Performance riskMinimalCan delay render if callback is heavy
SSR (Node.js)SafePrints a warning
Typical useAPIs, subscriptions, timersDOM measurements, style corrections

How it works internally

During React's commit phase, useLayoutEffect callbacks run synchronously right after DOM mutations, before the browser paints. useEffect callbacks are queued after paint via React's internal Scheduler at a lower priority. In React 18 concurrent mode, useLayoutEffect still fires synchronously and will block transitions. For non-urgent work inside a startTransition call, keep it in useEffect.

Common mistakes

Using useEffect for scroll corrections:

jsx
// Wrong: user sees top of page first, then it jumps useEffect(() => { ref.current.scrollTop = 100; }, []); // Correct: scroll happens before first paint useLayoutEffect(() => { ref.current.scrollTop = 100; }, []);

Putting heavy computation inside useLayoutEffect:

jsx
// Wrong: blocks paint for 100ms+, UI appears frozen useLayoutEffect(() => { for (let i = 0; i < 1_000_000; i++) { /* heavy work */ } }, []);

Keep useLayoutEffect callbacks under 5ms. Move anything heavier to useEffect.

Forgetting SSR compatibility: useLayoutEffect triggers a warning in Node.js: Warning: useLayoutEffect does nothing on the server. If the component renders on the server (Next.js, Remix), switch to useEffect or guard with typeof window !== 'undefined'.

Real-world usage

  • Framer Motion reads DOM layout in useLayoutEffect before writing animation values
  • Next.js <Image> uses useLayoutEffect for placeholder sizing
  • TanStack Query v5 uses useEffect for fetches, useLayoutEffect for table cache sync
  • React DevTools uses useLayoutEffect internally for inspector measurements
  • Redux Toolkit uses useEffect for store subscriptions

Follow-up questions

Q: Can useLayoutEffect cause performance problems?
A: Yes. It blocks paint, so if the callback takes more than a few milliseconds the browser frame is delayed and the UI feels janky. Chrome DevTools flags this as a long task.

Q: What happens to useLayoutEffect in SSR?
A: React prints a warning and the hook does nothing on the server. useEffect is SSR-safe because it only runs in the browser.

Q: What is the cleanup order for both hooks?
A: Both support cleanup functions. useLayoutEffect cleanup runs synchronously before the next layout effect fires. useEffect cleanup runs asynchronously before the next effect.

Q: In React 18 concurrent mode, does useLayoutEffect still block?
A: Yes, it remains synchronous and pre-paint even in concurrent mode. startTransition defers state updates, but useLayoutEffect still fires before paint. Use useEffect for non-urgent side effects inside transitions.

Examples

Measuring element height before paint

jsx
function TodoItem({ text, onDelete }) { const ref = useRef(); const [height, setHeight] = useState(0); useLayoutEffect(() => { // Measure before paint - no visible layout jump setHeight(ref.current.getBoundingClientRect().height); }); return ( <div ref={ref} style={{ minHeight: `${height}px`, transition: 'height 0.2s' }}> {text} <button onClick={onDelete}>Delete</button> </div> ); }

getBoundingClientRect reads real DOM dimensions. In useEffect the browser would paint the wrong height first, causing a visible jump on add or delete. useLayoutEffect measures and corrects before the user sees anything.

Data fetching with useEffect

jsx
function UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { let cancelled = false; fetch(`/api/users/${userId}`) .then(res => res.json()) .then(data => { if (!cancelled) setUser(data); }); return () => { cancelled = true; }; }, [userId]); if (!user) return <p>Loading...</p>; return <p>{user.name}</p>; }

Data fetching has nothing to do with pre-paint layout. useEffect is correct here. The cancelled flag prevents a state update on an unmounted component, which is a common source of memory leak warnings in React DevTools.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?