If you have spent any time with React hooks, you have probably leaned on useEffect for most side-effect work. But there is a lesser-known sibling, useLayoutEffect, that becomes essential the moment you need to read or manipulate the DOM before the browser has a chance to paint.
Pair it with useRef, and you have a precise tool for layout-sensitive logic without the visual flicker that a regular effect can sometimes cause.
A quick refresher on useRef
useRef gives you a mutable container whose .current property persists across renders without triggering a re-render when it changes. Its most common use is holding a reference to a DOM element.
const boxRef = useRef(null);
return <div ref={boxRef}>Hello</div>;
After the component mounts, boxRef.current points directly to the underlying DOM node, giving you access to properties like offsetHeight, getBoundingClientRect(), and anything else the browser exposes on real elements.
The problem useLayoutEffect solves
Here is where things get interesting. useEffect runs after the browser has already painted the screen. That is fine for fetching data or setting up subscriptions, but it becomes a problem when you need to measure or adjust the DOM before the user sees anything.
If you read an element's height in useEffect and then update state based on it, the user may briefly see the wrong layout before the correction kicks in. That is the flicker this hook is meant to avoid.
useLayoutEffect fires synchronously after all DOM mutations but before the browser paints. That gives you a small window to inspect and adjust the DOM in a way that is completely invisible to the user.
A practical example
Imagine you want to position a tooltip so it never overflows the viewport. You need to measure the tooltip after it renders, then nudge it back into place.
import { useRef, useState, useLayoutEffect } from 'react';
function Tooltip({ text }) {
const tooltipRef = useRef(null);
const [offset, setOffset] = useState(0);
useLayoutEffect(() => {
const rect = tooltipRef.current.getBoundingClientRect();
const overflow = rect.right - window.innerWidth;
if (overflow > 0) {
setOffset(-overflow - 8);
}
}, [text]);
return (
<div
ref={tooltipRef}
style={{ transform: `translateX(${offset}px)` }}
className="tooltip"
>
{text}
</div>
);
}
Here, useRef gives us direct access to the tooltip DOM node. useLayoutEffect then reads its position after React has applied the render but before the browser shows it to the user. The correction happens invisibly, with no flash and no visual jump.
The mental model
1 React renders
Virtual DOM changes are committed to the real DOM.
2 useLayoutEffect fires
You can read measurements and synchronously apply corrections.
3 Browser paints
The user sees the final, corrected result.
4 useEffect fires
This is the right place for async work that does not need to block paint.
useRef is the bridge between the React world and the real DOM. useLayoutEffect is the timing mechanism that lets you act on that bridge at exactly the right moment.
When to use it and when not to
Reach for useLayoutEffect when you need to measure DOM geometry, synchronize animations, or prevent layout flicker caused by a two-pass render. For everything else, including data fetching, logging, and subscriptions, stick with useEffect.
Heavy work inside useLayoutEffect will block paint and make the app feel sluggish. It also does not run on the server, so if you are working with server-side rendering you may need to guard against it or defer the logic.
Understanding the distinction between these hooks turns what looks like an obscure edge case into a reliable pattern. The next time you see a flash of misaligned content, there is a good chance useLayoutEffect paired with useRef is exactly the tool you need.