Suggest an editImprove this articleRefine the answer for “How useLayoutEffect works in React and how does it differ from useEffect?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**useLayoutEffect** runs synchronously after React commits DOM changes, before the browser paints. **useEffect** runs asynchronously after paint. ```jsx useLayoutEffect(() => { readDOM(); updateDOM(); }, []); // sync, pre-paint useEffect(() => { fetchData(); }, []); // async, post-paint ``` **Key rule:** need to read or modify the DOM before the user sees it? `useLayoutEffect`. Everything else: `useEffect`.Shown above the full answer for quick recall.Answer (EN)Image**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 | Aspect | useLayoutEffect | useEffect | |---|---|---| | **Timing** | Sync, post-commit, pre-paint | Async, post-paint | | **Blocks browser paint?** | Yes | No | | **Best for** | DOM measurements, style corrections | API calls, subscriptions, logging | | **SSR behavior** | Skipped entirely (no DOM) | Runs normally | | **Performance cost** | Higher (blocks UI thread) | Lower (non-blocking) | | **Strict Mode in dev** | Double-invoked | Double-invoked | | **React 18 concurrent mode** | Immediate priority, can block transitions | Passive, 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.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.