React

useEffect vs useLayoutEffect: when the result changes

useEffect runs after the browser paints, useLayoutEffect runs before it and synchronously: the second one matters when you must measure the DOM first.

Key points

  • useLayoutEffect is synchronous and blocks the paint: use it only when a DOM measurement must happen before the visible frame.
  • useEffect starts after the paint and stays the right choice for data, subscriptions and logging.
  • On the server useLayoutEffect cannot measure anything: React warns and the fix arrives only after hydration.

The difference is timing: useEffect runs after the browser paints, useLayoutEffect runs before the paint and synchronously. When the result depends on a DOM measurement the second one wins, otherwise the first one costs less.

The order inside one frame

React updates the DOM, runs the layout effects, lets the browser paint, then runs the passive effects. The layout effect is synchronous: the paint waits until the function returns.

  • Render: React computes the new tree and compares it with the previous one.
  • Layout effect: runs right after the DOM write, before the user sees the frame.
  • Paint: the browser draws what it has on the page.
  • Effect: useEffect runs afterwards, blocking nothing.

The case where the result changes

A tooltip that must centre on its anchor does not know its own width until it sits in the DOM. With useEffect the first frame shows it offset and the second one corrects it: the eye sees the jump. With useLayoutEffect the measurement and the correction happen before the paint.

function Tooltip({ anchor, children }) {
  const ref = useRef(null);
  const [left, setLeft] = useState(0);

  useLayoutEffect(() => {
    const width = ref.current.getBoundingClientRect().width;
    setLeft(anchor.x - width / 2);
  }, [anchor.x]);

  return (
    
      {children}
    
  );
}

When useEffect is the right choice

Data, subscriptions, logs and the page title do not need to stop the paint: a passive effect lets the browser draw and starts right after. The practical rule is to begin with useEffect and switch only when a visible measurement demands it.

The cost

Every layout effect lengthens the frame: when it reads and writes the DOM in a loop, the browser waits. A setState inside useLayoutEffect forces an extra synchronous render, and a long list of those components is felt while scrolling.

Trying it locally

npm create vite@latest effect-demo -- --template react-ts
cd effect-demo
npm install
npm run dev

Vite's react-ts template, under the MIT licence, includes React 19 and TypeScript: open src/App.tsx, replace the content with the component above and watch the page while the window changes width. If the same component ends up in a server render, React prints a warning: there is no DOM to measure on the server.

Live records in the catalogue

Two records show the problem in production: dropdown-menu, which computes its position on Radix primitives before the paint, and theme-toggle, which swaps its icon without showing the intermediate frame. The rest sits among the 111,985 components and the 1,556 blocks.

The limitation

On a server-rendered component useLayoutEffect cannot measure anything: the HTML arrives without the correction and the right value appears only after hydration. For that case the measurement moves to a passive effect or gets solved in CSS.

Where to continue

The order of calls is in rules of hooks; what changes in version 19 is in React 19; the comparison of framework cost on the first render is in frontend frameworks compared.

Frequently asked questions

Why is useEffect not enough to measure the DOM?

Because it starts after the paint: the measurement arrives once the user has already seen the wrong frame. useLayoutEffect runs earlier and lets you correct the style without showing the step.

Does useLayoutEffect run on the server?

No. There is no layout to measure on the server, so React skips it and defers the effect to hydration, with a console warning during the server render.

Which one should read localStorage?

useEffect, because the value does not depend on layout. The first frame shows the default and the second shows the stored value: if that jump is visible, read the value in a client-only initialisation effect.

Can useLayoutEffect cause an infinite loop?

Yes, when the effect updates state that appears in its dependency array. The loop is synchronous, so the page freezes instead of slowing down: React warns with “Maximum update depth exceeded”.

How do I see the difference in DevTools?

In the Performance panel, the time between the DOM write and the first paint includes the layout effects. Record the same component twice, with each hook, to see what the synchronous version costs.