useLayoutEffect

Runs an effect after DOM mutation but before the browser paints, so you can measure and reposition without flicker.

Signature
useLayoutEffect(setup: () => (void | (() => void)), dependencies?: ReadonlyArray<any>): void
  • Added in React 16.8.0

Parameters

ParameterTypeDescription
$setup() => void | (() => void)The function with your Effect's logic. It may optionally return a cleanup function. After your component commits to the DOM and before the browser repaints the screen, React runs your setup function. After every commit with changed dependencies, React first runs the cleanup function with the old values, then runs setup with the new values. Before the component is removed from the DOM, React runs the cleanup function.
$dependenciesoptionalReadonlyArray<any> | undefinedThe list of all reactive values referenced inside the setup code — props, state, and every variable and function declared directly inside the component body. The list must have a constant number of items and be written inline, like [dep1, dep2, dep3]. React compares each dependency with its previous value using Object.is. If you omit this argument, the Effect re-runs after every commit of the component.

Return value

undefinedReturns undefined.

Errors

React throws an "Invalid hook call" error if you call it outside the top level of a component or custom Hook, and warns in development when it is reached during server rendering, where it does nothing.

Changelog

  • PHP 16.8.0Introduced with Hooks.
  • PHP 18.0.0In development Strict Mode, React runs one extra setup + cleanup cycle before the first real setup.
  • PHP 18.0.0Triggering a state update inside useLayoutEffect makes React flush all remaining Effects immediately, including useEffect, before the browser paints.

useLayoutEffect has the same signature and the same cleanup rules as useEffect; only the timing differs. React runs it synchronously after the DOM has been mutated but before the browser paints, and any state update you schedule from it is processed in the same blocking pass. The user therefore never sees the intermediate frame.

When to reach for it

Only when you must read layout and then change it in the same frame. The canonical cases are measuring a node with getBoundingClientRect or offsetHeight and using the result to decide where something renders — a tooltip that flips above the trigger when it would overflow the viewport, a popover aligned to an anchor, a list that restores scroll position after items are prepended. With useEffect, the same code paints the wrong position first and then corrects it, which reads as a flicker.

What trips people up

  • It blocks paint. Every layout Effect in the tree runs before the frame is shown, so overusing it makes an app feel sluggish. Default to useEffect and switch only when you can see the flicker.
  • It does not run during server rendering, and React warns about it. For a component that must render on the server, guard the measurement or defer to useEffect on the first pass.
  • Refs are attached by the time it runs, but child layout Effects run before parent ones.

Example

Try it yourself
Loading editor…
Loading React…

When to use it

  • Measure a tooltip and flip it above the trigger when it would overflow the bottom of the viewport.
  • Read a list's scroll height after prepending items and restore the previous scroll offset before paint.
  • Size a textarea to its content on the first render without a visible one-frame jump.
  • Align a dropdown to its anchor element after layout, before the user sees it.

Frequently asked questions

What is the difference between useEffect and useLayoutEffect?

useLayoutEffect runs synchronously after the DOM is mutated but before the browser paints, while useEffect normally runs after paint. Both take the same setup and dependency arguments and follow the same cleanup rules; the only reason to choose the layout variant is that the user would otherwise see an incorrect frame, and the cost is that it blocks painting.

Why does React warn that useLayoutEffect does nothing on the server?

Layout Effects only run on the client, so a component rendered with renderToString produces markup that has never been measured. Either move the measurement to useEffect and accept one corrected frame, render a deliberate unmeasured fallback on the server, or make the component client-only — silencing the warning with a mounted flag just restores the flicker.

Can I set state inside useLayoutEffect?

Yes, and that is the normal pattern for measure-then-position code; React processes the update and re-renders before the browser paints. Be careful that the state you set is not also a dependency of the same Effect, and remember that a state update from a layout Effect forces React to flush every remaining Effect, including useEffect, in the same blocking pass.

More examples

A tooltip that flips above its trigger when it would overflow

The tooltip has to render once before its height is knowable, then immediately reposition. Because the measurement and the resulting state update both happen inside `useLayoutEffect`, React finishes the second render before releasing the frame, so the user only ever sees the final position. Swapping the Hook for `useEffect` produces a visible one-frame jump: the tooltip appears at `targetRect.bottom`, off the bottom of the viewport, and then snaps upward.

Example · jsx
import { useState, useRef, useLayoutEffect } from "react";

function Tooltip({ targetRect, children }) {
  const ref = useRef(null);
  const [tooltipHeight, setTooltipHeight] = useState(0);

  useLayoutEffect(() => {
    setTooltipHeight(ref.current.getBoundingClientRect().height);
  }, []);

  let top = targetRect.bottom;
  if (top + tooltipHeight > window.innerHeight) {
    // Not enough room below — render it above the trigger instead.
    top = targetRect.top - tooltipHeight;
  }

  return (
    <div
      ref={ref}
      style={{
        position: "fixed",
        left: targetRect.left,
        top,
        background: "#222",
        color: "white",
        padding: "4px 8px"
      }}
    >
      {children}
    </div>
  );
}

export default function TooltipDemo() {
  const buttonRef = useRef(null);
  const [rect, setRect] = useState(null);

  function show() {
    setRect(buttonRef.current.getBoundingClientRect());
  }

  return (
    <div style={{ padding: 40 }}>
      <button ref={buttonRef} onMouseEnter={show} onMouseLeave={() => setRect(null)}>
        Hover me
      </button>
      {rect && <Tooltip targetRect={rect}>Positioned before paint</Tooltip>}
    </div>
  );
}

Restoring scroll position after prepending items

Prepending content grows `scrollHeight` above the viewport, which normally shoves the visible messages downward. The layout Effect compares the new `scrollHeight` against the value recorded on the previous commit and adds the difference to `scrollTop` before paint, so the message the user was reading stays on exactly the same pixel row. Done in `useEffect`, the correction happens after the browser has already painted the shifted list, producing a visible jump.

Example · jsx
import { useState, useRef, useLayoutEffect } from "react";

export default function MessageLog() {
  const listRef = useRef(null);
  const prevHeightRef = useRef(0);
  const [messages, setMessages] = useState(["message 3", "message 4"]);

  useLayoutEffect(() => {
    const list = listRef.current;
    const added = list.scrollHeight - prevHeightRef.current;
    if (prevHeightRef.current !== 0 && added > 0) {
      // Keep the visible messages visually still while older ones load above.
      list.scrollTop += added;
    }
    prevHeightRef.current = list.scrollHeight;
  }, [messages]);

  function loadOlder() {
    setMessages((prev) => ["older " + prev.length, ...prev]);
  }

  return (
    <div>
      <button onClick={loadOlder}>Load older</button>
      <div ref={listRef} style={{ height: 120, overflowY: "auto" }}>
        {messages.map((m) => (
          <p key={m}>{m}</p>
        ))}
      </div>
    </div>
  );
}

Discussion

  • Be the first to comment on this lesson.