Skip to the solution
HardFrontend6 min read

When would you use useLayoutEffect instead of useEffect?

514 views

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.

  • hooks
  • useLayoutEffect
  • useEffect
  • dom

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

6 min read Β· 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactA tooltip positioned before paint, next to one that visibly jumps
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>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 101 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextHard

cache() and fetch memoization β€” request deduplication in RSC

Continue