Skip to main content

How useLayoutEffect works in React and how does it differ from useEffect?

useLayoutEffect runs synchronously after React commits DOM changes but before the browser paints the screen. useEffect runs asynchronously after paint. That timing difference is the whole point.

Theory

TL;DR

  • useLayoutEffect is like proofreading a letter before sealing the envelope. useEffect is checking what you sent after delivery.
  • Main difference: useLayoutEffect blocks browser paint until it finishes. useEffect does not.
  • Reading or modifying the DOM before the user sees it? useLayoutEffect. Data fetching, subscriptions, analytics? useEffect.
  • useLayoutEffect is a no-op during SSR (no DOM). useEffect runs normally.
  • In React 18, useLayoutEffect still runs at immediate priority and can block concurrent transitions.

Quick example

jsx
import { useEffect, useLayoutEffect, useRef } from "react"; function Demo() { const ref = useRef(null); useLayoutEffect(() => { // Runs BEFORE paint - user never sees this intermediate state if (ref.current) ref.current.style.background = "red"; }); useEffect(() => { // Runs AFTER paint - overwrites the layout effect if (ref.current) ref.current.style.background = "blue"; }); return <div ref={ref} style={{ width: 100, height: 100, background: "green" }} />; } // Visible result: blue box. // Order: green (JSX) -> red (layout effect, pre-paint) -> blue (effect, post-paint). // The user only ever sees blue - no flicker.

Both effects fire on every render here (no dependency array). The layout effect sets red, but that change never reaches the screen because paint is still blocked. The passive effect fires after paint and overwrites to blue.

Key difference

useLayoutEffect hooks into React's commit phase at commitLayoutEffects, which runs synchronously and blocks requestAnimationFrame until it completes. So you can call getBoundingClientRect() and get values that reflect the latest committed DOM state. useEffect is scheduled as a passive effect at lower priority, after the browser has already painted. Any DOM read inside useEffect may reflect already-rendered values, which produces the visual jump you've probably seen with tooltips or popovers that reposition themselves on first render.

When to use useLayoutEffect vs useEffect

  • Measure element dimensions before the user sees them: useLayoutEffect. Avoids the flash where an element jumps from one position to another.
  • Read scroll position or text selection after a commit: useLayoutEffect. The data matches the current DOM state, not a stale snapshot.
  • Position tooltips, dropdowns, or popovers: useLayoutEffect. Reading layout and writing back in the same synchronous window prevents flicker.
  • Data fetching, subscriptions, analytics, timers: useEffect. These do not need pre-paint access. Blocking paint for them just wastes render time.

Comparison table

AspectuseLayoutEffectuseEffect
TimingSync, post-commit, pre-paintAsync, post-paint
Blocks browser paint?YesNo
Best forDOM measurements, style correctionsAPI calls, subscriptions, logging
SSR behaviorSkipped entirely (no DOM)Runs normally
Performance costHigher (blocks UI thread)Lower (non-blocking)
Strict Mode in devDouble-invokedDouble-invoked
React 18 concurrent modeImmediate priority, can block transitionsPassive, deferred

How React schedules these internally

React's fiber reconciler runs useLayoutEffect during commitLayoutEffects at ImmediateSchedulerPriority. It fires in a synchronous loop over fiber roots, completing before requestAnimationFrame gets a slot. useEffect goes into commitPassiveMountEffects, scheduled via scheduleCallback(NormalSchedulerPriority). Passive and deferred, so the browser paints first, then React processes the passive effects queue.

But there is a catch. In React 18 concurrent mode, useLayoutEffect still runs at immediate priority. Heavy work inside it can delay transitions started by useTransition, because it does not yield to deferred updates.

In practice, I have seen more than a few codebases where useLayoutEffect was used for data fetching. The network request is async either way, so it does not help. But it adds blocking time to every render cycle. Open Chrome DevTools Performance tab and record a render - you'll see it clearly labeled in the timeline.

Common mistakes with useLayoutEffect

Fetching data inside useLayoutEffect

jsx
// Wrong - blocks paint for no reason useLayoutEffect(() => { fetch('/api/data').then(setData); }, []); // Right useEffect(() => { fetch('/api/data').then(setData); }, []);

The fetch is async regardless. useLayoutEffect does not make it faster. It just delays the first paint.

Ignoring server-side rendering

jsx
// Logs a warning in SSR - no DOM on the server useLayoutEffect(() => { document.title = 'My App'; }, []); // Safe alternatives: useEffect(() => { document.title = 'My App'; }, []); // Or guard it: useLayoutEffect(() => { if (typeof window === 'undefined') return; document.title = 'My App'; }, []);

Next.js and Remix skip useLayoutEffect on the server but log a dev warning. If you see that warning, switch to useEffect or add the typeof window guard.

Unstable dependencies cause infinite loops

jsx
// Infinite loop - new object reference on every render useLayoutEffect(() => { applyStyle(elementRef.current, style); }, [style]); // style = { color: 'red' } is recreated each render // Fix with useMemo const style = useMemo(() => ({ color: 'red' }), []); useLayoutEffect(() => { applyStyle(elementRef.current, style); }, [style]);

This is not unique to useLayoutEffect, but the synchronous re-render batching makes the loop tighter and harder to spot.

Using it for animations that run on every frame

jsx
// Blocks requestAnimationFrame on every value change useLayoutEffect(() => { gsap.to(element, { x: 100 }); }, [value]); // Better - let the animation library own the frame loop useEffect(() => { gsap.to(element, { x: 100 }); }, [value]);

Blocking requestAnimationFrame inside a layout effect causes frame drops. Animation libraries manage their own scheduling - do not interrupt that.

Real-world usage

  • React Window / React Virtual: measures row heights before virtualizing scroll using useLayoutEffect so the initial render shows correct scroll offsets.
  • Framer Motion: reads DOM bounds pre-paint for layout animations (the layout prop internally).
  • Floating UI / React Tooltip: all tooltip and popover positioning runs in useLayoutEffect so elements appear at the correct coordinates on first render.
  • TanStack Table: header resize measurements go through useLayoutEffect to avoid column-width flicker during drag.
  • React Spring: uses layout effects for synchronous style reads in physics-based animations.

Follow-up questions

Q: What is the exact execution order of useLayoutEffect and useEffect including cleanups on re-render?
A: On mount: layout effect runs, then passive effect runs. On re-render: layout cleanup fires, layout effect fires, then passive cleanup fires, then passive effect fires. Layout always precedes passive. This matters when one effect depends on state set by the other.

Q: What happens to useLayoutEffect in SSR (Next.js, Remix)?
A: React skips it and logs a dev warning. The component renders, but the effect never runs on the server. Use useEffect for server-safe side effects.

Q: In React 18 Strict Mode, how many times does useLayoutEffect fire?
A: Twice in development. React mounts, runs the effect, runs the cleanup, then remounts. This surfaces missing cleanup logic. In production it fires once.

Q: What is useInsertionEffect and how does it compare?
A: useInsertionEffect (React 18+) fires before DOM mutations. It was designed for CSS-in-JS libraries that inject stylesheets and cannot read layout at all. useLayoutEffect fires after mutations and can read layout. For general DOM work, useLayoutEffect is still the right choice.

Q (senior): Can useLayoutEffect block concurrent transitions in React 18?
A: Yes. It runs at ImmediateSchedulerPriority, so it executes before React yields to deferred updates from useTransition. Heavy synchronous logic inside it will delay transition completion and visibly slow the UI.

Examples

Tooltip positioning without flicker

This is the canonical use case. Without useLayoutEffect, the tooltip briefly renders at position {top: 0, left: 0} and then jumps to the correct coordinates.

jsx
import { useLayoutEffect, useState, useRef } from "react"; function Tooltip({ children, text }) { const [position, setPosition] = useState({ top: 0, left: 0 }); const triggerRef = useRef(null); const tooltipRef = useRef(null); useLayoutEffect(() => { const trigger = triggerRef.current.getBoundingClientRect(); const tooltip = tooltipRef.current.getBoundingClientRect(); // Calculate position pre-paint so no jump on first render setPosition({ top: trigger.bottom + 8, left: Math.max(8, trigger.left + (trigger.width - tooltip.width) / 2), }); }, []); return ( <> <span ref={triggerRef}>{children}</span> <div ref={tooltipRef} style={{ position: "fixed", top: position.top, left: position.left, background: "#111", color: "#fff", padding: "4px 8px", borderRadius: 4, }} > {text} </div> </> ); }

Both element rects are read synchronously, the position is calculated, and React re-renders with the correct coordinates before the browser paints. Floating UI uses this same pattern internally.

Detecting text overflow before the first paint

jsx
import { useState, useLayoutEffect, useRef } from "react"; function TruncatedLabel({ text }) { const [isOverflowing, setIsOverflowing] = useState(false); const ref = useRef(null); useLayoutEffect(() => { if (ref.current) { // scrollWidth > clientWidth means text is clipped setIsOverflowing(ref.current.scrollWidth > ref.current.clientWidth); } }, [text]); return ( <span ref={ref} title={isOverflowing ? text : undefined} style={{ display: "block", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", maxWidth: 200, }} > {text} </span> ); }

With useEffect, the component would paint once without the title attribute, then add it on the next tick. With useLayoutEffect, the measurement and re-render with the correct title attribute happen before the first paint.

Using useEffect for the common case

Not everything needs useLayoutEffect. For anything that does not involve DOM measurements or mutations before paint, useEffect is the right choice.

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

Data fetching does not need pre-paint access. Using useLayoutEffect here blocks the browser's first paint while the request is in flight - extra delay, zero benefit.

Short Answer

Interview ready
Premium

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

Finished reading?