Skip to solution
hardFrontend

When would you use `useLayoutEffect` instead of `useEffect`?

514 views
01

Understand the problem

Question presented to candidate: "When is useLayoutEffect the right choice, and what does it cost you?"

What a strong answer should cover:

  • Both run after React has committed changes to the DOM. The difference is relative to paint: useLayoutEffect runs synchronously before the browser paints; useEffect runs asynchronously after.
  • The use case: measure the DOM and adjust it in the same frame, so the user never sees the intermediate state.
  • Concrete cases: positioning a tooltip or popover from a measured rect, measuring text to decide truncation, restoring scroll position, and preventing a visible flash on a mount-time adjustment.
  • The cost is real: it blocks painting. Slow work there delays the frame, and a long layout effect is directly visible as jank.
  • It runs on every commit where its dependencies change, exactly like useEffect.
  • It does not run during SSR — neither does useEffect — but React warns specifically about useLayoutEffect on the server because layout measurement is meaningless there.
  • useInsertionEffect sits even earlier, for CSS-in-JS libraries injecting styles.
  • The default is useEffect; reach for the layout variant only when you can name the flicker it prevents.

Clarifying questions expected:

  • "Is there a visible flicker, or is this a habit?" — if nothing flashes, useEffect is correct.
  • "Is this server-rendered?" — that changes what warns.

Code / implementation expected: Yes — a measure-then-position example where the difference is visible.

hooksuseLayoutEffectuseEffectdom
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for React interviews — assumes useEffect. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. The ordering in section 3 was measured on React 19.2.8. *One honest limit:

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

A tooltip positioned before paint, next to one that visibly jumps
Run Playground
import { useState, useRef, useEffect, useLayoutEffect } from "react";

// Both tooltips measure the trigger and position themselves above it. The only
// difference is WHICH hook does the measuring — and therefore whether the user
// sees the unpositioned frame first.
function Tooltip({ text, useLayout, label }) {
  const [open, setOpen] = useState(false);
  const triggerRef = useRef(null);
  const tipRef = useRef(null);
  const [pos, setPos] = useState({ top: 0, left: 0 });

  const measure = () => {
    if (!open || !triggerRef.current || !tipRef.current) return;
    const t = triggerRef.current.getBoundingClientRect();
    const tip = tipRef.current.getBoundingClientRect();
    // Deliberate extra work so the difference is easy to see.
    let waste = 0;
    for (let i = 0; i < 2_000_000; i++) waste += i;
    setPos({ top: t.top - tip.height - 8 + window.scrollY, left: t.left + window.scrollX });
  };

  // Only one of these is active per instance.
  useLayoutEffect(() => { if (useLayout) measure(); }, [open, useLayout]);
  useEffect(() => { if (!useLayout) measure(); }, [open, useLayout]);

  return (
    <>
      <button
        ref={triggerRef}
        onClick={() => setOpen((o) => !o)}
        style={{ marginRight: 12 }}
      >
        {label}
      </button>
      {open && (
        <div
          ref={tipRef}
          style={{
            position: "absolute", top: pos.top, left: pos.left,
            background: "#222", color: "white", padding: "6px 10px",
            borderRadius: 6, fontSize: 13, whiteSpace: "nowrap", zIndex: 10,
          }}
        >
          {text}
        </div>
      )}
    </>
  );
}

export default function App() {
  return (
    <div style={{ padding: 24, paddingTop: 140, fontFamily: "system-ui", lineHeight: 1.6 }}>
      <Tooltip
        label="useLayoutEffect (no jump)"
        text="Measured and positioned before paint"
        useLayout={true}
      />
      <Tooltip
        label="useEffect (visible jump)"
        text="Painted at 0,0 first, then moved"
        useLayout={false}
      />

      <p style={{ marginTop: 40, color: "#666", fontSize: 13 }}>
        Click each button. The first tooltip appears directly above its trigger.
        The second flashes at the top-left corner of the page before jumping into
        place — it was painted once with the default position, then corrected.
        <br /><br />
        Both do identical work. Only the timing relative to paint differs, which
        is also the cost: the first one delays the frame while it measures.
      </p>
    </div>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 101 of 119 decoded in the React.js track. One more won't hurt.

Back to track