useLayoutEffect
useLayoutEffect is the synchronous sibling of useEffect. It fires after React has mutated the DOM but before the browser paints the screen, which gives you a chance to measure layout and make corrections that the user never sees as a flicker. Because it blocks painting, it is a precision tool: reach for it only when you genuinely need to read or adjust the DOM in the same frame, and prefer useEffect for everything else.
When it runs
Both hooks run after React commits changes to the DOM. The difference is timing relative to the browser’s paint:
| Hook | Runs | Blocks paint? |
|---|---|---|
useEffect | After the browser paints | No — non-blocking, asynchronous |
useLayoutEffect | After DOM mutation, before paint | Yes — synchronous |
The sequence for a render that uses useLayoutEffect is: render → React mutates the DOM → useLayoutEffect fires (and any state update it triggers is re-rendered) → browser paints. The user sees only the final result of that whole sequence, so any visual adjustment you make happens invisibly.
import { useLayoutEffect, useRef } from "react";
function Measured() {
const ref = useRef(null);
useLayoutEffect(() => {
const { width, height } = ref.current.getBoundingClientRect();
console.log("measured before paint:", width, height);
}, []);
return <div ref={ref}>Measure me</div>;
}
Output:
measured before paint: 240 24
The signature is identical to useEffect: a setup function that may return a cleanup function, plus an optional dependency array compared with Object.is.
Measuring layout
The classic use case is reading the actual size or position of a DOM node and using it to position something else — a tooltip, a popover, a custom dropdown. If you measured in useEffect, the element would paint at the wrong place first, then jump to the correct spot. useLayoutEffect does the measurement and correction in the same frame, so there is no jump.
import { useLayoutEffect, useRef, useState } from "react";
function Tooltip({ targetRef, children }) {
const tooltipRef = useRef(null);
const [style, setStyle] = useState({ visibility: "hidden" });
useLayoutEffect(() => {
const target = targetRef.current.getBoundingClientRect();
const tip = tooltipRef.current.getBoundingClientRect();
setStyle({
position: "fixed",
top: target.top - tip.height - 8,
left: target.left + target.width / 2 - tip.width / 2,
visibility: "visible",
});
}, [targetRef]);
return (
<div ref={tooltipRef} style={style}>
{children}
</div>
);
}
The tooltip renders hidden, gets measured and positioned synchronously, then becomes visible — all before the first paint, so the user never sees it in the wrong place.
Avoiding flicker
Any time a render shows a temporary state that you immediately correct, useEffect leaks that intermediate frame to the screen. Consider a list that should auto-scroll to the bottom when a new message arrives.
import { useLayoutEffect, useRef } from "react";
function MessageList({ messages }) {
const listRef = useRef(null);
useLayoutEffect(() => {
const el = listRef.current;
el.scrollTop = el.scrollHeight;
}, [messages]);
return (
<ul ref={listRef} style={{ height: 200, overflowY: "auto" }}>
{messages.map((m) => (
<li key={m.id}>{m.text}</li>
))}
</ul>
);
}
With useLayoutEffect the scroll position is set before paint, so the new message appears already scrolled into view. The same code in useEffect would briefly show the old scroll position and then snap, producing a visible flicker.
Rule of thumb: if your effect reads layout (
getBoundingClientRect,offsetWidth,scrollHeight) and then sets state or mutates the DOM based on what it read, useuseLayoutEffect. If it talks to a network, a timer, or a subscription, useuseEffect.
The SSR warning
useLayoutEffect cannot run on the server — there is no DOM to measure and no paint to block. React skips it during server rendering, which means any layout correction is absent from the server-generated HTML. If your component relies on useLayoutEffect to look right, React will log a warning during SSR:
Warning: useLayoutEffect does nothing on the server, because its effect
cannot be encoded into the server renderer's output format.
You have a few options:
- Move the logic to
useEffectif a one-frame flicker on hydration is acceptable. - Render the component only on the client (e.g. behind a mounted flag) so the layout effect always has a DOM.
- Use
useSyncExternalStorefor external state that differs between server and client.
import { useLayoutEffect, useEffect } from "react";
// A hook that picks the right effect for the environment.
const useIsomorphicLayoutEffect =
typeof window !== "undefined" ? useLayoutEffect : useEffect;
This useIsomorphicLayoutEffect pattern is common in libraries: it uses useLayoutEffect in the browser and silently falls back to useEffect on the server to suppress the warning.
When to prefer useEffect
useLayoutEffect runs synchronously and delays the paint, so overusing it makes the UI feel sluggish — especially with heavy work or large component trees. Default to useEffect and only escalate when you can see a flicker without it.
// ❌ Blocks paint for work that doesn't need to
useLayoutEffect(() => {
fetchAnalytics();
}, []);
// ✅ Non-blocking — paint first, side effect after
useEffect(() => {
fetchAnalytics();
}, []);
Data fetching, logging, subscriptions, and timers have no visual ordering requirement, so they belong in useEffect.
Best Practices
- Use
useLayoutEffectonly to read layout and synchronously adjust the DOM before paint; default touseEffectotherwise. - Keep the work inside it minimal — it blocks the browser from painting until it finishes.
- Always include a complete dependency array, just as with
useEffect, and return cleanup where needed. - Guard against the SSR warning with the
useIsomorphicLayoutEffectfallback when rendering on the server. - Pair measurement with a
useRefon the node you need to read. - If you only need to expose an imperative handle, reach for
useImperativeHandlerather than measuring manually.