10LOC
Reactintermediate

useLayoutEffect vs useEffect: measuring the DOM before paint

Published November 24, 2026

import { useRef, useState, useLayoutEffect } from "react";

export const Tooltip = ({ text }: { text: string }) => {
  const [height, setHeight] = useState(0);
  const ref = useRef<HTMLDivElement>(null);

  useLayoutEffect(() => {
    setHeight(ref.current?.getBoundingClientRect().height ?? 0);
  }, [text]);

  return (
    <div>
      <div ref={ref}>{text}</div>
      <div style={{ marginTop: height + 8 }}>{height}px tall, positioned below</div>
    </div>
  );
};

What

useLayoutEffect runs synchronously after React updates the DOM but before the browser paints, making it the right tool for reading layout (like an element's height) and writing state back based on it without a visible flash.

Why it matters

useEffect runs after paint. If you measure a DOM node in a useEffect and use the result to set state that changes layout, the browser has already painted the old layout once, then paints again a frame later once the effect's setState commits — a visible flicker, worse the slower the measurement or the update. useLayoutEffect runs at a point where React can apply the DOM change before anything reaches the screen, trading a small amount of blocking time for a layout that's correct on the very first paint.

How it works

  • ref is attached to the content whose height needs to be measured.
  • useLayoutEffect runs after the DOM has text committed but before paint; getBoundingClientRect().height reads the real, laid-out height at that point.
  • setHeight updates state synchronously within the same browser task, so React re-renders and commits the new layout before the paint that was about to happen — no intermediate frame with the wrong height is ever shown.
  • The dependency on text re-measures whenever the content that affects height changes.

Gotchas

  • useLayoutEffect blocks the browser from painting until it (and any state updates inside it) finish — overusing it for anything that isn't layout-critical measurement will make the whole page feel less responsive.
  • It does nothing useful during server rendering; React warns if it runs in a component rendered on the server without being guarded to the client, since there's no DOM to measure yet.
  • If you don't need the measured value to affect this render's layout — just want to react to a DOM change after the fact — useEffect is the right choice and won't block paint.

Related

  • useEffect — the default choice for anything that isn't specifically about avoiding a layout flash: subscriptions, data fetching, logging.
  • ResizeObserver — for tracking size changes that happen after mount, independent of React's render cycle (e.g., a user resizing the window).