skip to content

In React, how does the timing of useLayoutEffect differ from useEffect, and what kind of bug makes you reach for useLayoutEffect?

level: middleimportance: must knowfreq 72%

answer

  1. both run after the DOM changes
  2. the difference is the paint
  3. one blocks the frame, one waits for it
  4. measure, then correct, unseen
  5. default stays the passive one

basics

~20 s

useLayoutEffect runs synchronously after React updates the DOM but before the browser paints, so React blocks the paint until it finishes; useEffect is deferred until after paint. Reach for useLayoutEffect when a measure-then-adjust step would otherwise flash.

solid answer

~50 s

Both hooks take the same arguments and both run after React has mutated the DOM — the difference is where the browser's paint sits. `useLayoutEffect` runs synchronously in the commit, before paint, and React flushes any state update it schedules before letting the browser draw, so the user only ever sees the corrected frame. `useEffect` is a passive effect: React lets the browser paint first and runs it afterwards, which keeps rendering responsive. The bug that forces `useLayoutEffect` is the measure-then-adjust flicker: you read a DOM measurement — an element's height, a tooltip's position against the viewport — and write it back as state or a style. With `useEffect` the user sees one frame at the pre-adjustment position and then a jump; with `useLayoutEffect` both happen inside the same frame. The cost is real: it is synchronous work on the critical path, so anything slow there directly delays the paint.

code

javascript · 16 lines
javascript
import { useLayoutEffect, useRef, useState } from 'react';

function Tooltip({ open }) {
  const ref = useRef(null);
  const [flipped, setFlipped] = useState(false);

  useLayoutEffect(() => {
    if (!open || !ref.current) return;
    const { right } = ref.current.getBoundingClientRect();
    setFlipped(right > window.innerWidth);
  }, [open]);

  return null;
}

export default Tooltip;

go deeper

for a junior

Remember the one-line difference: useLayoutEffect runs before the browser paints, useEffect runs after. Know that useEffect is the default you should reach for, and that both accept the same dependency array.

for a middle

Walk the commit ordering — DOM mutation, layout effects and their flushed updates, paint, then passive effects — and give the measure-then-adjust flicker as the concrete case. Say why blocking paint is the cost you are paying.

for a senior

Show you weigh the cost: layout effects sit on the critical path to the first pixel, forced layout reads are expensive, and a state update there buys an extra render inside the same frame. Be able to decide from a bug report which hook the code should have used.

for a principal

Own the guidance for a codebase: a default of passive effects, a named justification required for each layout effect, and awareness that the pattern often signals layout that CSS could have handled without a measurement round trip at all.

## The shared half `useEffect(setup, deps?)` and `useLayoutEffect(setup, deps?)` have identical signatures, identical dependency semantics — positional comparison with `Object.is` — and the identical cleanup contract. Both run after React has already applied its DOM mutations for that commit, so in both hooks the DOM is up to date and refs are attached. Everything that differs comes down to one question: has the browser painted yet? ## Where the paint sits Within a commit, the order is: 1. React mutates the DOM to match the new render output. 2. React runs every `useLayoutEffect` cleanup and setup, **synchronously**, and flushes any state update they schedule — re-rendering and re-mutating as needed. 3. The browser paints. 4. React runs the `useEffect` cleanups and setups. So `useLayoutEffect` sits between the DOM being correct and the pixels being drawn. The browser cannot paint until step 2 finishes, which is precisely the property you are buying: whatever you do there is invisible to the user because they never see the intermediate state. `useEffect` is classified as a *passive* effect and deliberately deferred so that painting is not held up by work that does not affect what the frame should look like. One caveat worth stating so you do not over-claim: React does not promise that passive effects always land after a paint. If another update arrives that needs them flushed, React may run them earlier. Treat "after paint" as the design intent and the normal case, never as a correctness guarantee your code depends on. ## The bug that forces the layout hook The canonical case is read-then-write of layout in the same commit. A tooltip renders at a provisional position, you measure its box against the viewport with the ref you just attached, discover it overflows the right edge, and set state to flip it to the other side. With `useEffect`, the sequence is: paint the tooltip on the wrong side, run the effect, set state, render, paint again. Two frames, and the user perceives a flash or a jump. With `useLayoutEffect`, the measurement and the corrective state update happen before the browser is allowed to draw, so only the final position is ever visible. ```js useLayoutEffect(() => { const { right } = ref.current.getBoundingClientRect(); if (right > window.innerWidth) setFlipped(true); }, [open]); ``` The same reasoning applies to restoring a scroll position after content changes, or syncing a dimension into state that a sibling renders against. The pattern to recognise is: *I must read a value only the DOM knows, and act on it before the frame is shown.* ## Why it is not the default Everything in a layout effect is on the critical path to the first pixel. Heavy work there — an expensive measurement loop, a synchronous library call, a cascade of state updates each triggering another render — directly extends the time before anything appears. The default should stay `useEffect`, with `useLayoutEffect` reserved for the cases where you can name the flicker it prevents. There is also a discipline point: reading layout with something like `getBoundingClientRect` forces the browser to compute layout on demand. Doing that inside a synchronous pre-paint block, repeatedly, is how you turn a smooth interaction into a janky one. Measure once, act once. ## How to answer the "which do I use" question A clean decision rule: **if the user would see a wrong frame without it, use `useLayoutEffect`; otherwise use `useEffect`.** Sending analytics, wiring a subscription, setting a document title, kicking off a request — none of those change what the current frame should look like, so they belong after paint. ## The third one React also exposes `useInsertionEffect(setup, deps?)`, which runs before layout effects and exists for CSS-in-JS libraries that need to inject style rules before anything reads layout. It is a library-author tool, not an application hook, but naming it correctly shows you understand that the commit has a defined ordering rather than one undifferentiated "after render" moment. ## The trap in the comparison The common wrong answer is that `useLayoutEffect` runs *before* the DOM is updated, as if it were a pre-render hook. It does not — it runs after mutation, before paint. Both hooks see the same DOM; only the audience differs, because in one case the user has already seen a frame and in the other they have not.

  • If useLayoutEffect avoids the flicker, why not use it everywhere?
    Because it is synchronous work standing between the DOM being ready and the browser painting. Every millisecond spent there delays the first pixel, and state updates scheduled inside it force an extra render-and-mutate cycle before the frame is allowed out. Use it only when you can name the wrong frame it prevents; otherwise the passive hook keeps painting off the critical path.
  • Does useLayoutEffect run before React updates the DOM?
    No — that is the usual misconception. React mutates the DOM first, then runs layout effects, then lets the browser paint. That ordering is exactly what makes measurement possible: the element you are about to measure is already in the document with its new content, and its ref is attached.
  • Where does useInsertionEffect fit in that ordering?
    It runs before layout effects in the same commit and exists so CSS-in-JS libraries can inject style rules before anything reads layout. It is intended for library authors rather than application code — you should not be reading refs or measuring in it — but it shows the commit has three distinct effect phases, not one.
  • A state update inside useLayoutEffect causes a second render. Does the user see the first one?
    No. React flushes that update and its re-render synchronously before yielding to the browser, so both the original and the corrected DOM exist before any paint. That is the whole point of the hook — but it also means you have paid for two renders in a single frame, which is why heavy or repeated updates there hurt.

saying these in an interview costs you the question

  • Says useLayoutEffect runs before React updates the DOM
  • Claims useEffect runs synchronously right after render
  • Uses useLayoutEffect for data fetching or subscriptions
  • Thinks the two hooks differ in dependency-array behaviour
  • Cannot name a concrete flicker useLayoutEffect prevents

context