Skip to content
React rc hooks 4 min read

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:

HookRunsBlocks paint?
useEffectAfter the browser paintsNo — non-blocking, asynchronous
useLayoutEffectAfter DOM mutation, before paintYes — 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, use useLayoutEffect. If it talks to a network, a timer, or a subscription, use useEffect.

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 useEffect if 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 useSyncExternalStore for 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 useLayoutEffect only to read layout and synchronously adjust the DOM before paint; default to useEffect otherwise.
  • 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 useIsomorphicLayoutEffect fallback when rendering on the server.
  • Pair measurement with a useRef on the node you need to read.
  • If you only need to expose an imperative handle, reach for useImperativeHandle rather than measuring manually.
Last updated June 14, 2026
Was this helpful?