ForgeFrontend — Prepare, Practice, Crack
Secure checkout
Lifetime access
Instant PDF download
Free updates forever

Prepare · Practice · Crack

useEffect vs useLayoutEffect

Short answer

Both run side effects after React updates the DOM. useEffect runs asynchronously after the browser has painted, so the user sees the first frame immediately. useLayoutEffect runs synchronously before paint, blocking the screen until it finishes. Default to useEffect; switch only to stop a visible flicker.

useEffectuseLayoutEffect
RunsAfter the browser paintsAfter DOM mutation, before paint
Blocks paintNoYes
TimingAsynchronous, deferredSynchronous, in the commit phase
User can see the intermediate stateYes — for one frameNo
Safe during SSRYes (skipped on the server)No — logs a warning
Typical useFetching, subscriptions, logging, timersMeasuring the DOM and adjusting it
Cost of overusing itLowHigh — it delays every paint
Default choiceAlways start hereOnly to fix a visible flicker
Same API, same arguments, one difference — where they sit relative to paint.

The timeline React actually follows

Every update goes through the same sequence: React runs your component function, works out what changed, and writes those changes into the real DOM. That write is the commit. Immediately after the commit — while the browser has new DOM but has not yet drawn anything — React runs every useLayoutEffect. Only when they have all finished does it hand control back, the browser paints, and useEffect runs afterwards.

Proving the orderjsx
1function Demo() {
2  useEffect(() => console.log("3. useEffect"));
3  useLayoutEffect(() => console.log("2. useLayoutEffect"));
4  console.log("1. render");
5  return <p>hi</p>;
6}
7
8// → 1. render
9// → 2. useLayoutEffect      (DOM updated, screen still shows the OLD frame)
10// → 3. useEffect            (runs after the browser painted the new frame)

The ordering inside the component is irrelevant — useLayoutEffect always wins, because React schedules the two lists in different phases rather than in call order. That is the whole difference, and everything else on this page is a consequence of it.

What the difference looks like on screen

For most effects the distinction is invisible. It becomes visible the moment an effect changes something the user can see — because with useEffect the browser has already drawn the pre-effect version, so the user gets one frame of the wrong thing and then a correction. That is the flicker.

The flicker, and the fixjsx
1// With useEffect the box renders at its default height, paints,
2// THEN jumps to the measured height. The user sees the jump.
3function Panel() {
4  const ref = useRef(null);
5  const [height, setHeight] = useState(0);
6
7  useEffect(() => {
8    setHeight(ref.current.getBoundingClientRect().height);
9  }, []);
10  // → frame 1: height 0   (painted)
11  // → frame 2: height 240 (painted) — visible jump
12
13  return <div ref={ref} style={{ height }}>…</div>;
14}
15
16// With useLayoutEffect the measurement and the second render both
17// happen before the browser is allowed to paint at all.
18useLayoutEffect(() => {
19  setHeight(ref.current.getBoundingClientRect().height);
20}, []);
21// → frame 1: height 240 (painted) — the 0 state never reaches the screen

Notice what makes that work: calling a state setter inside useLayoutEffect causes React to re-render and re-commit synchronously, before yielding to the browser. Do the same thing inside useEffect and the extra render lands in a later frame — which is precisely the frame the user perceives as a flash.

Measuring the DOM is the real use case

Almost every legitimate useLayoutEffect has the same shape: read a layout value the browser can only produce after rendering, then use it to position or size something. Tooltips, popovers, dropdowns that flip when they would overflow the viewport, auto-growing textareas and virtualised lists that need row heights all fall into this bucket.

A tooltip that flips above the trigger when there is no room belowjsx
1function Tooltip({ anchorRef, children }) {
2  const tipRef = useRef(null);
3  const [placement, setPlacement] = useState("bottom");
4
5  useLayoutEffect(() => {
6    const anchor = anchorRef.current.getBoundingClientRect();
7    const tip = tipRef.current.getBoundingClientRect();
8    const roomBelow = window.innerHeight - anchor.bottom;
9
10    setPlacement(roomBelow < tip.height ? "top" : "bottom");
11    // → decided and re-committed BEFORE paint, so the tooltip never
12    //   appears in the wrong place for a frame
13  }, [anchorRef]);
14
15  return <div ref={tipRef} data-placement={placement}>{children}</div>;
16}

The give-away is getBoundingClientRect, offsetHeight, scrollHeight, getComputedStyle or window.getSelection appearing inside the effect. Those values do not exist until the DOM is live, and acting on them after paint means the user watched you get it wrong first.

The server-rendering warning

There is no layout on the server. renderToString produces an HTML string with no browser, no box model and nothing to measure, so React skips useLayoutEffect during SSR and warns that your effect did nothing. If the effect was cosmetic the page still works, but hydration renders the unadjusted markup first — the flicker you were trying to avoid, now on the slowest possible path.

The isomorphic-effect pattern used by every major libraryjsx
1import { useEffect, useLayoutEffect } from "react";
2
3// On the server there is no window, so fall back to useEffect,
4// which React skips during SSR without complaining.
5export const useIsomorphicLayoutEffect =
6  typeof window !== "undefined" ? useLayoutEffect : useEffect;
7
8// → browser: behaves as useLayoutEffect
9// → server:  behaves as useEffect, so no
10//   "useLayoutEffect does nothing on the server" warning

This is not a trick to silence a warning — it is the correct behaviour. The warning is telling you the effect genuinely cannot run there, and the fallback makes that explicit. In Next.js the same problem appears in any component that is not client-only, which is worth saying out loud in an interview because it shows you have shipped SSR code.

This is 1 of 75+ questions in the React Interview Kit

Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.

⚡ Get the React Interview Kit → ₹399

Cleanup timing differs too

The returned cleanup function inherits the schedule of the effect that created it. A useLayoutEffect cleanup runs synchronously before the next paint, so tearing down the old value and setting up the new one both complete in a single frame. A useEffect cleanup runs after paint, which leaves a window where the old subscription is gone and the new one has not started.

Same code, two different orderings on updatejsx
1useLayoutEffect(() => {
2  console.log("setup", id);
3  return () => console.log("cleanup", id);
4}, [id]);
5// id 1 → 2 produces, all before one paint:
6// → cleanup 1
7// → setup 2
8
9useEffect(() => {
10  console.log("setup", id);
11  return () => console.log("cleanup", id);
12}, [id]);
13// → same sequence, but after the browser already painted the new
14//   frame — the DOM you measure in cleanup may already be gone

This matters when cleanup needs to read the node it is detaching from — saving a scroll position, for example. By the time a useEffect cleanup runs, React may already have removed the node, so the read returns zeros. A useLayoutEffect cleanup still sees it in place.

The performance cost, concretely

A frame at 60fps has about 16 milliseconds. Everything inside your useLayoutEffect — plus any re-render it triggers, plus the layout the browser must recalculate when you read a measured property — comes out of that budget before a single pixel is drawn. One tooltip is nothing. Fifty list rows each measuring themselves is a visibly janky page, and on a mid-range Android phone the threshold is far lower than on your laptop.

The effect…HookWhy
Fetches datauseEffectAsync anyway; paint first, show a skeleton
Subscribes to an event or storeuseEffectNothing visible depends on the first frame
Sets a timer or intervaluseEffectTiming is not tied to paint
Logs analytics or page viewsuseEffectNever delay the user for telemetry
Measures a node, then positions ituseLayoutEffectOtherwise the wrong position paints first
Restores scroll position on mountuseLayoutEffectA visible jump otherwise
Reads the DOM in cleanup before unmountuseLayoutEffectThe node still exists at cleanup time
Syncs a third-party widget's sizeuseLayoutEffectLayout thrash is visible if deferred
Choosing by what the effect actually does.

React 18 and 19: what changed

Concurrent rendering did not change the relationship between the two hooks — useLayoutEffect is still before paint, useEffect still after. What changed is that renders can be interrupted, so an effect assuming it runs exactly once per visible update has to be idempotent. Strict Mode in development also mounts, unmounts and remounts every component once, running both effects twice on purpose to expose missing cleanup.

React 18 also added useInsertionEffect, which runs earlier still — before React touches the DOM at all. It exists for CSS-in-JS libraries injecting style tags, and application code should never need it. If a question asks about effect ordering in full, the sequence is useInsertionEffect, then useLayoutEffect, then paint, then useEffect.

When neither is the right answer

A surprising number of measure-then-adjust effects can be deleted entirely. CSS solves most positioning problems without JavaScript, ResizeObserver reacts to size changes without re-measuring on every render, and CSS anchor positioning now handles tooltip flipping natively in Chromium. An effect that never runs beats either hook.

Observing instead of measuring on every renderjsx
1useEffect(() => {
2  const el = ref.current;
3  const ro = new ResizeObserver(([entry]) => {
4    setWidth(entry.contentRect.width);
5  });
6  ro.observe(el);
7  return () => ro.disconnect();
8  // → fires on every size change, including ones no React render
9  //   caused: font load, container query, window resize
10}, []);

That version uses useEffect deliberately: the observer's first callback fires after paint, which is fine because the element already had a correct size from CSS. You only need useLayoutEffect when a correct first frame is impossible without JavaScript.

If useLayoutEffect always avoids the flicker, why not use it everywhere?

Because every one of them is a synchronous block on the frame the user is waiting for. Ten components each doing two milliseconds of work in useLayoutEffect adds twenty milliseconds to first paint — more than a frame — on every update. useEffect costs nothing, because by then the user is already looking at the result.

Does useLayoutEffect run before or after the ref is attached?

After. React attaches refs during the commit phase, before layout effects fire, which is exactly why ref.current is safe to read inside useLayoutEffect. It is also why reading ref.current in the render body is not safe — that runs before the commit.

What is the class component equivalent?

componentDidMount and componentDidUpdate fire synchronously before paint, so they behave like useLayoutEffect, not useEffect. That surprises people migrating a class to hooks: swapping in useEffect can introduce a flicker the class version never had.

Can I call a state setter inside useLayoutEffect?

Yes, and that is the point — React re-renders and re-commits synchronously before yielding to the browser, so the intermediate state is never painted. Guard it with a condition, though: an unconditional setState in a layout effect with no dependency array is an infinite loop that freezes the tab.

Which runs first when a parent and a child both have effects?

The child, for both hooks — React walks the tree bottom-up during commit. All layout effects across the whole tree complete before any paint, then all passive effects run in the same child-first order.

Frequently asked questions

What is the difference between useEffect and useLayoutEffect?
Timing relative to paint. useLayoutEffect runs synchronously after React has updated the DOM but before the browser draws anything, so it blocks the frame. useEffect runs asynchronously after the browser has painted. Same signature, same dependency rules — only the schedule differs.
What is useLayoutEffect in React?
A hook that runs a side effect synchronously in the commit phase, after React writes changes to the DOM and before the browser paints. It exists so you can read layout values like getBoundingClientRect and act on them without the user seeing the pre-adjustment state.
When should I use useLayoutEffect instead of useEffect?
When the effect changes something visible based on a measurement of the DOM, and running it after paint produces a visible flicker or jump. Positioning tooltips and popovers, restoring scroll position, and auto-sizing a textarea are the classic cases.
Does useLayoutEffect block rendering?
It blocks painting, not React's render. React has already produced and committed the new DOM; the browser simply is not allowed to draw it until every layout effect has returned. Slow work inside one adds directly to the time before the user sees anything.
Why does useLayoutEffect warn during server-side rendering?
There is no DOM and no layout on the server, so React cannot run it and tells you the effect did nothing. Use the isomorphic pattern — pick useLayoutEffect when window exists and useEffect otherwise — which is what Radix, MUI and React Router do internally.
Is useLayoutEffect slower than useEffect?
The hook itself is not slower; its position in the frame is what costs you. The same work delays first paint in useLayoutEffect and does not in useEffect. On a slow device, several layout effects doing measurements can push a frame past 16ms and produce visible jank.
Which runs first, useEffect or useLayoutEffect?
useLayoutEffect, always, no matter which one you wrote first in the component. React keeps them in separate queues, flushes all layout effects during commit, then paints, then flushes the passive effects.
Can I use useLayoutEffect for data fetching?
You can, but there is no benefit. A fetch resolves asynchronously long after paint either way, so all you have done is block the first frame on the synchronous setup. Fetching belongs in useEffect, or better, in a data library or a server component.
Does useEffect run on every render?
It runs after every render whose dependency array changed. No array means every render; an empty array means only on mount. The rule is identical for useLayoutEffect — dependencies control whether it runs, the hook choice controls when.
Why do my effects run twice in development?
Strict Mode deliberately mounts, unmounts and remounts every component once in development, so both hooks fire twice. It is a test that your cleanup is correct, not a bug — and it does not happen in production builds.
Is componentDidMount like useEffect or useLayoutEffect?
useLayoutEffect. Class lifecycle methods run synchronously before paint, so a class-to-hooks migration that swaps componentDidMount for useEffect can introduce a flicker the class version never had. When one appears after a migration, this is usually why.
What is useInsertionEffect and how does it fit in?
It runs before React mutates the DOM, earlier than both, and exists for CSS-in-JS libraries that must inject style tags before layout is read. Application code should not need it. The complete order is useInsertionEffect, useLayoutEffect, paint, useEffect.

This is 1 of 75+ questions in the React Interview Kit

Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.

⚡ Get the React Interview Kit → ₹399
Written by Arun Karthikeyan · Last updated

Full kit

React Interview Kit · ₹399

Get it →