| |

React 48 ⚛️ Synchronous DOM Reads with useLayoutEffect

⏱️ Estimated read: 20 min | |

useEffect is the hook that runs the side effects after the render is committed to the screen. The browser has already painted the DOM, and the effect runs asynchronously, after the paint. For most effects — the data fetching, the subscriptions, the logging — this is exactly right. The effect does not need to run before the paint, and running it after keeps the browser responsive. But some effects must run before the browser paints. They read the DOM, measure the element, and update the state based on the measurement. If they run after the paint, the user sees the wrong layout for a frame: the element is rendered in the wrong position, then the effect measures it, then the state updates, then the element moves. The flash is visible, and it is the bug that useLayoutEffect exists to fix.

The distinction between useEffect and useLayoutEffect is the timing. useEffect runs asynchronously, after the browser paints. useLayoutEffect runs synchronously, after the DOM is mutated but before the browser paints. The effect can read the DOM, measure the element, and trigger a synchronous re-render, and the user sees only the final result. The block is the whole point: the browser waits for the layout effect to finish before it paints, so the effect’s synchronous work is invisible. The cost is the blocking: the layout effect delays the paint, and a slow layout effect makes the page feel sluggish.

The rule of thumb is to use useEffect by default and to reach for useLayoutEffect only when the effect reads the DOM and updates the state based on the read. The measurement, the scroll position, the tooltip positioning, the animation initial state — these are the layout effects. The data fetching, the event subscriptions, the analytics — these are the regular effects. The useLayoutEffect in the server-side rendering produces the warning, because the server has no DOM to measure, and the warning is the reminder that the layout effect is the browser-only hook.

This chapter covers three areas. First, why useLayoutEffect exists — the pre-paint measurement problem and the flicker. Second, how the timing works — the order of the render, the DOM mutation, the layout effect, the paint, and the passive effect. Third, the patterns and the pitfalls — the measurement, the tooltip, the scroll, the SSR warning, and the cases where the layout effect hurts more than it helps. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the effect timing.

Key point: useLayoutEffect runs synchronously after the DOM is mutated but before the browser paints. It is for the effects that read the DOM and update the state based on the read, so the user never sees the intermediate state. useEffect runs asynchronously after the paint, and it is the default. The useLayoutEffect blocks the paint, and it produces a warning in the server-side rendering.


Why useLayoutEffect exists

The pre-paint measurement problem. The browser renders the DOM in two steps: the layout and the paint. The layout computes the position and the size of the elements, and the paint draws them to the screen. A component that needs to measure its size must read the layout, and the read is valid only after the layout. The useEffect runs after the paint, so the measurement happens after the user has seen the element. The useLayoutEffect runs after the layout but before the paint, so the measurement happens before the user sees the element.

The flicker problem. The classic example is the tooltip. The tooltip is rendered at the default position, the useEffect measures the trigger, the state updates, and the tooltip moves to the correct position. The user sees the tooltip at the wrong position for a frame, then at the correct position. The flicker is the visible bug. The useLayoutEffect measures the trigger and updates the state before the paint, so the tooltip is painted at the correct position the first time.

The scroll-position problem. A component that scrolls to the bottom on the mount must read the scroll height and set the scroll top. With the useEffect, the element is painted at the top, then the effect scrolls it to the bottom, and the user sees the jump. With the useLayoutEffect, the scroll happens before the paint, and the user sees only the bottom.

The animation-initial-state problem. An animation that starts from a computed position must set the initial state before the paint. The useLayoutEffect computes the position and applies the initial transform, and the animation starts from the correct place. The useEffect would let the element paint at the default position, then jump to the computed position.

The synchronous-re-render problem. The useLayoutEffect can trigger the synchronous re-render. When the effect calls the setState, React re-renders the component synchronously, before the browser paints. The user sees only the final render, and the intermediate render is invisible. The useEffect triggers the asynchronous re-render, and the intermediate render is painted.

The trade-off. The useLayoutEffect blocks the paint. The browser waits for the effect to finish before it draws the screen. A slow layout effect — a heavy measurement, a synchronous loop, the layout thrashing — makes the page feel sluggish, because the paint is delayed. The useEffect does not block the paint, and the page stays responsive. The trade-off is between the correctness of the pre-paint measurement and the responsiveness of the post-paint effect. The useLayoutEffect is the tool for the cases where the correctness matters.


a. The timing order

The order of the operations in the React commit phase is fixed. The useLayoutEffect and the useEffect are at the different points in the order.

1. The render phase
   - React calls the component function.
   - React computes the new element tree.
   - React reconciles the tree with the previous one.

2. The DOM mutation
   - React applies the changes to the DOM.
   - The browser updates the DOM tree.
   - The DOM is now the "committed" state.

3. The layout effects (useLayoutEffect)
   - React runs the useLayoutEffect callbacks synchronously.
   - The callbacks can read the DOM (the layout is computed).
   - The callbacks can setState, which triggers a synchronous re-render.
   - The re-render's effects run before the paint.

4. The browser paint
   - The browser paints the DOM to the screen.

5. The passive effects (useEffect)
   - React runs the useEffect callbacks asynchronously.
   - The callbacks run after the paint.
   - The callbacks can setState, which triggers an asynchronous re-render.

The key insight is the position of the two effects relative to the paint. The useLayoutEffect is before the paint, and the useEffect is after. The useLayoutEffect is the last chance to read the DOM and change the output before the user sees it.

The cleanup order. The cleanup functions run in the reverse order. When the component re-renders, the previous useLayoutEffect‘s cleanup runs before the new useLayoutEffect, and the previous useEffect‘s cleanup runs before the new useEffect. The layout cleanups run before the paint, and the passive cleanups run after.

The synchronous re-render. The useLayoutEffect that calls the setState triggers the synchronous re-render. React re-renders the component, applies the new DOM mutations, runs the layout effects again (including the new ones), and then paints. The user sees only the final state. The useEffect that calls the setState triggers the asynchronous re-render, and the intermediate state is painted.


b. The basic useLayoutEffect

The useLayoutEffect has the same signature as the useEffect: the callback and the dependency array.

import { useLayoutEffect, useRef, useState } from 'react';

function Tooltip({ targetRef, children }) {
  const [position, setPosition] = useState({ top: 0, left: 0 });
  const tooltipRef = useRef(null);

  useLayoutEffect(() => {
    const target = targetRef.current;
    const tooltip = tooltipRef.current;

    if (!target || !tooltip) return;

    const targetRect = target.getBoundingClientRect();
    const tooltipRect = tooltip.getBoundingClientRect();

    setPosition({
      top: targetRect.top - tooltipRect.height - 8,
      left: targetRect.left + (targetRect.width - tooltipRect.width) / 2,
    });
  }, [targetRef]);

  return (
    <div
      ref={tooltipRef}
      style={{
        position: 'fixed',
        top: position.top,
        left: position.left,
      }}
    >
      {children}
    </div>
  );
}

The useLayoutEffect measures the target and the tooltip with the getBoundingClientRect, computes the position, and calls the setPosition. The call triggers the synchronous re-render, and the tooltip is painted at the correct position the first time. The useEffect would let the tooltip paint at the top: 0, left: 0 position, then move it, and the user would see the jump.

The dependency array. The useLayoutEffect takes the dependency array, and the effect re-runs when the dependencies change. The empty array runs the effect once, on the mount. The missing array runs the effect on every render, which is the source of the performance problems.

The cleanup. The useLayoutEffect can return the cleanup function, and the cleanup runs before the next effect and on the unmount.

useLayoutEffect(() => {
  const subscription = subscribe();
  return () => subscription.unsubscribe();
}, []);

The cleanup is the same as the useEffect, and the timing is the difference.


c. The patterns

The measurement pattern. The component measures the element and updates the state based on the measurement.

function AutoSizeBox({ children }) {
  const [height, setHeight] = useState(0);
  const boxRef = useRef(null);

  useLayoutEffect(() => {
    if (boxRef.current) {
      setHeight(boxRef.current.getBoundingClientRect().height);
    }
  }, [children]);

  return (
    <div ref={boxRef} style={{ height: height || 'auto' }}>
      {children}
    </div>
  );
}

The useLayoutEffect measures the box and updates the height before the paint. The useEffect would let the box paint at the auto height, then set the measured height, and the user would see the resize.

The scroll pattern. The component scrolls the element to the position before the paint.

function Chat({ messages }) {
  const listRef = useRef(null);

  useLayoutEffect(() => {
    if (listRef.current) {
      listRef.current.scrollTop = listRef.current.scrollHeight;
    }
  }, [messages]);

  return (
    <div ref={listRef} style={{ height: 200, overflow: 'auto' }}>
      {messages.map((m) => (
        <p key={m.id}>{m.text}</p>
      ))}
    </div>
  );
}

The useLayoutEffect scrolls the list to the bottom before the paint. The useEffect would let the list paint at the top, then scroll, and the user would see the jump.

The tooltip pattern. The component positions the tooltip relative to the trigger before the paint. The pattern is the same as the measurement.

The animation pattern. The component sets the initial transform before the animation starts.

function SlideIn({ children }) {
  const [offset, setOffset] = useState(100);
  const ref = useRef(null);

  useLayoutEffect(() => {
    setOffset(0);
  }, []);

  return (
    <div
      ref={ref}
      style={{
        transform: `translateX(${offset}px)`,
        transition: 'transform 0.3s',
      }}
    >
      {children}
    </div>
  );
}

The useLayoutEffect sets the offset to 0 before the paint, and the transition animates from the initial 100 to the 0. The useEffect would let the element paint at the 100, then set the 0, and the animation would start from the painted 100.

The SSR warning. The useLayoutEffect produces the warning in the server-side rendering, because the server has no DOM. The React documentation recommends the useEffect for the components that may be server-rendered, and the useLayoutEffect for the client-only components. The useIsomorphicLayoutEffect is the pattern that uses the useLayoutEffect on the client and the useEffect on the server:

import { useEffect, useLayoutEffect } from 'react';

const useIsomorphicLayoutEffect =
  typeof window !== 'undefined' ? useLayoutEffect : useEffect;

export default useIsomorphicLayoutEffect;

The custom hook selects the useLayoutEffect when the window is defined (the client) and the useEffect when it is not (the server). The pattern is the standard for the libraries that support the SSR.

The layout-thrashing pattern. The useLayoutEffect that reads the DOM and writes the DOM repeatedly causes the layout thrashing: the browser recomputes the layout on every read after the write. The pattern is to batch the reads and the writes, and to use the requestAnimationFrame for the deferred work.

useLayoutEffect(() => {
  // The reads first.
  const heights = items.map((item) => item.ref.current.offsetHeight);
  // The writes after.
  items.forEach((item, i) => {
    item.ref.current.style.height = `${heights[i]}px`;
  });
}, [items]);

The reads first, then the writes, is the pattern that avoids the thrashing.


Complete Example Session

// ============================================
// PART 1: THE TOOLTIP WITH useLayoutEffect
// ============================================

import { useLayoutEffect, useRef, useState } from 'react';

function Tooltip({ targetRef, children }) {
  const [position, setPosition] = useState({ top: 0, left: 0 });
  const tooltipRef = useRef(null);

  useLayoutEffect(() => {
    const target = targetRef.current;
    const tooltip = tooltipRef.current;

    if (!target || !tooltip) return;

    const targetRect = target.getBoundingClientRect();
    const tooltipRect = tooltip.getBoundingClientRect();

    setPosition({
      top: targetRect.top - tooltipRect.height - 8,
      left: targetRect.left + (targetRect.width - tooltipRect.width) / 2,
    });
  }, [targetRef]);

  return (
    <div
      ref={tooltipRef}
      style={{
        position: 'fixed',
        top: position.top,
        left: position.left,
      }}
    >
      {children}
    </div>
  );
}

// The tooltip is painted at the correct position the first time.
// The useEffect would let it paint at the top: 0, left: 0 first.


// ============================================
// PART 2: THE SAME WITH useEffect (THE FLICKER)
// ============================================

function TooltipFlicker({ targetRef, children }) {
  const [position, setPosition] = useState({ top: 0, left: 0 });
  const tooltipRef = useRef(null);

  useEffect(() => {
    // The same measurement.
    setPosition({ ... });
  }, [targetRef]);

  return <div ref={tooltipRef} style={{ top: position.top }}>{children}</div>;
}

// The tooltip paints at the top: 0, then moves to the position.
// The user sees the flicker.


// ============================================
// PART 3: THE SCROLL
// ============================================

function Chat({ messages }) {
  const listRef = useRef(null);

  useLayoutEffect(() => {
    if (listRef.current) {
      listRef.current.scrollTop = listRef.current.scrollHeight;
    }
  }, [messages]);

  return (
    <div ref={listRef} style={{ height: 200, overflow: 'auto' }}>
      {messages.map((m) => (
        <p key={m.id}>{m.text}</p>
      ))}
    </div>
  );
}

// The list is painted scrolled to the bottom.


// ============================================
// PART 4: THE MEASUREMENT
// ============================================

function AutoSizeBox({ children }) {
  const [height, setHeight] = useState(0);
  const boxRef = useRef(null);

  useLayoutEffect(() => {
    if (boxRef.current) {
      setHeight(boxRef.current.getBoundingClientRect().height);
    }
  }, [children]);

  return (
    <div ref={boxRef} style={{ height: height || 'auto' }}>
      {children}
    </div>
  );
}

// The box is painted at the measured height.


// ============================================
// PART 5: THE ANIMATION INITIAL STATE
// ============================================

function SlideIn({ children }) {
  const [offset, setOffset] = useState(100);
  const ref = useRef(null);

  useLayoutEffect(() => {
    setOffset(0);
  }, []);

  return (
    <div
      ref={ref}
      style={{
        transform: `translateX(${offset}px)`,
        transition: 'transform 0.3s',
      }}
    >
      {children}
    </div>
  );
}

// The element is painted at the 0, and the transition starts
// from the 100.


// ============================================
// PART 6: THE THEME FLICKER
// ============================================

function ThemeProvider({ theme, children }) {
  useLayoutEffect(() => {
    document.documentElement.dataset.theme = theme;
  }, [theme]);

  return children;
}

// The theme is applied before the paint.
// The useEffect would let the old theme paint, then the new one.


// ============================================
// PART 7: THE SSR WARNING
// ============================================

// The useLayoutEffect in the SSR produces:
// "useLayoutEffect does nothing on the server, because its effect
// cannot be encoded into the server renderer's output format."


// ============================================
// PART 8: THE useIsomorphicLayoutEffect
// ============================================

import { useEffect, useLayoutEffect } from 'react';

const useIsomorphicLayoutEffect =
  typeof window !== 'undefined' ? useLayoutEffect : useEffect;

export default useIsomorphicLayoutEffect;

// The client uses the useLayoutEffect, and the server uses the
// useEffect. The warning is gone.


// ============================================
// PART 9: THE LAYOUT THRASHING
// ============================================

function BatchingExample({ items }) {
  const refs = useRef([]);

  useLayoutEffect(() => {
    // The reads first.
    const heights = refs.current.map(
      (ref) => ref?.getBoundingClientRect().height ?? 0
    );

    // The writes after.
    refs.current.forEach((ref, i) => {
      if (ref) {
        ref.style.height = `${heights[i]}px`;
      }
    });
  }, [items]);

  return (
    <div>
      {items.map((item, i) => (
        <div key={item.id} ref={(el) => (refs.current[i] = el)}>
          {item.name}
        </div>
      ))}
    </div>
  );
}

// The reads and the writes are batched to avoid the thrashing.


// ============================================
// PART 10: THE COMPLETE EXAMPLE
// ============================================

function Dropdown({ triggerRef, items, open, onClose }) {
  const [position, setPosition] = useState({ top: 0, left: 0, width: 0 });
  const dropdownRef = useRef(null);

  useLayoutEffect(() => {
    if (!open) return;

    const trigger = triggerRef.current;
    const dropdown = dropdownRef.current;

    if (!trigger || !dropdown) return;

    const triggerRect = trigger.getBoundingClientRect();
    const dropdownRect = dropdown.getBoundingClientRect();

    const spaceBelow = window.innerHeight - triggerRect.bottom;
    const spaceAbove = triggerRect.top;

    const top =
      spaceBelow >= dropdownRect.height || spaceBelow >= spaceAbove
        ? triggerRect.bottom + 4
        : triggerRect.top - dropdownRect.height - 4;

    setPosition({
      top,
      left: triggerRect.left,
      width: triggerRect.width,
    });
  }, [open, triggerRef]);

  useEffect(() => {
    if (!open) return;

    function handleClickOutside(event) {
      if (!dropdownRef.current?.contains(event.target)) {
        onClose();
      }
    }

    document.addEventListener('mousedown', handleClickOutside);
    return () => document.removeEventListener('mousedown', handleClickOutside);
  }, [open, onClose]);

  if (!open) return null;

  return (
    <div
      ref={dropdownRef}
      style={{
        position: 'fixed',
        top: position.top,
        left: position.left,
        minWidth: position.width,
      }}
    >
      {items.map((item) => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}

// The useLayoutEffect positions the dropdown before the paint.
// The useEffect handles the click-outside subscription.
// The two hooks work together.

The ten parts show the tooltip with the useLayoutEffect, the same with the useEffect (the flicker), the scroll, the measurement, the animation initial state, the theme flicker, the SSR warning, the useIsomorphicLayoutEffect, the layout thrashing, and the complete example.


Quick Reference

The Two Hooks

AspectuseLayoutEffectuseEffect
The timingAfter the DOM mutation, before the paintAfter the paint
The blockingSynchronous; blocks the paintAsynchronous; does not block
The DOM readValidValid
The setStateThe synchronous re-renderThe asynchronous re-render
The SSRThe warningThe supported
The use caseThe measurement, the scroll, the positionThe data, the subscriptions, the logging

The Timing Order

OrderPhase
1The render
2The DOM mutation
3The useLayoutEffect
4The paint
5The useEffect

The Patterns

PatternThe hook
The tooltip positionuseLayoutEffect
The scroll to the bottomuseLayoutEffect
The element measurementuseLayoutEffect
The animation initial stateuseLayoutEffect
The theme applicationuseLayoutEffect
The data fetchinguseEffect
The event subscriptionuseEffect
The analytics logginguseEffect

The SSR

ApproachThe behavior
The useLayoutEffect on the serverThe warning
The useEffect on the serverThe no-op
The useIsomorphicLayoutEffectThe client: the layout; the server: the passive

The Anti-Patterns

The anti-patternThe problem
The heavy computationThe paint is blocked
The layout thrashingThe repeated read-write
The data fetchingThe paint is blocked
The subscriptionThe timing is the wrong

Best Practices

✅ Do This:

// Use the useLayoutEffect for the pre-paint measurement
useLayoutEffect(() => {
  setPosition(measure(element));
}, []);                                                                   // ✅
// Use the useEffect for the data fetching
useEffect(() => {
  fetchData().then(setData);
}, []);                                                                   // ✅
// Use the useIsomorphicLayoutEffect for the SSR
const useIsomorphicLayoutEffect = typeof window !== 'undefined'
  ? useLayoutEffect : useEffect;                                          // ✅
// Batch the reads and the writes
const heights = refs.map((r) => r.offsetHeight);
refs.forEach((r, i) => { r.style.height = `${heights[i]}px`; });          // ✅
// Use the useLayoutEffect for the scroll
useLayoutEffect(() => {
  listRef.current.scrollTop = listRef.current.scrollHeight;
}, [messages]);                                                           // ✅
// Keep the layout effects small
// The blocking is the cost                                                // ✅

❌ Don’t Do This:

// Don't use the useLayoutEffect for the data fetching
useLayoutEffect(() => {
  fetchData().then(setData);  // blocks the paint                          // ❌
}, []);
// Don't use the useLayoutEffect in the SSR
useLayoutEffect(() => {
  // the warning                                                          // ❌
}, []);
// Don't do the layout thrashing
refs.forEach((r) => {
  const h = r.offsetHeight;  // the read
  r.style.height = `${h}px`;  // the write after each read                 // ❌
});
// Don't use the useLayoutEffect for the logging
useLayoutEffect(() => {
  log('rendered');  // the timing is wrong                                 // ❌
}, []);
// Don't forget the dependency array
useLayoutEffect(() => {
  // runs on every render                                                  // ❌
});

Common Pitfalls

PitfallWhy It HappensFix
The SSR warningThe useLayoutEffect on the serverUse the useIsomorphicLayoutEffect
The slow paintThe heavy layout effectMove the work to the useEffect
The layout thrashingThe interleaved read-writeBatch the reads then the writes
The stale measurementThe missing dependencyAdd the dependency
The flickerThe useEffect instead of the layoutUse the useLayoutEffect
The infinite loopThe setState without the dependencyAdd the correct dependencies
The effect runs on every renderThe missing dependency arrayAdd the array

Real-World Examples

1. The Tooltip

useLayoutEffect(() => {
  setPosition(computePosition(target, tooltip));
}, [targetRef]);

2. The Scroll

useLayoutEffect(() => {
  listRef.current.scrollTop = listRef.current.scrollHeight;
}, [messages]);

3. The Measurement

useLayoutEffect(() => {
  setHeight(boxRef.current.getBoundingClientRect().height);
}, [children]);

4. The Animation

useLayoutEffect(() => {
  setOffset(0);
}, []);

5. The Theme

useLayoutEffect(() => {
  document.documentElement.dataset.theme = theme;
}, [theme]);

6. The Isomorphic

const useIsomorphicLayoutEffect =
  typeof window !== 'undefined' ? useLayoutEffect : useEffect;

7. The Batching

const heights = refs.map((r) => r.offsetHeight);
refs.forEach((r, i) => { r.style.height = `${heights[i]}px`; });

8. The Data Fetch

useEffect(() => {
  fetchData().then(setData);
}, []);

9. The Subscription

useEffect(() => {
  const sub = subscribe();
  return () => sub.unsubscribe();
}, []);

10. The Complete Dropdown

useLayoutEffect(() => {
  setPosition(computeDropdownPosition(trigger, dropdown));
}, [open]);

useEffect(() => {
  document.addEventListener('mousedown', handleClickOutside);
  return () => document.removeEventListener('mousedown', handleClickOutside);
}, [open]);

Visual

The Effect Timing

┌──────────────────────────────────────────────────────────────┐
│  THE EFFECT TIMING                                           │
│                                                              │
│  1. The render                                               │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  The component function runs.                        │    │
│  │  The element tree is computed.                       │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    ▼                                                         │
│  2. The DOM mutation                                         │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  The DOM is updated.                                 │    │
│  │  The layout is computed.                             │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    ▼                                                         │
│  3. The useLayoutEffect                                      │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  The effect runs synchronously.                      │    │
│  │  The DOM read is valid.                              │    │
│  │  The setState triggers the synchronous re-render.    │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    ▼                                                         │
│  4. The paint                                                │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  The browser paints the DOM.                         │    │
│  │  The user sees the final state.                      │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    ▼                                                         │
│  5. The useEffect                                            │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  The effect runs asynchronously.                     │    │
│  │  The DOM is already painted.                         │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Flicker

┌──────────────────────────────────────────────────────────────┐
│  THE FLICKER                                                 │
│                                                              │
│  WITH the useEffect:                                         │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  1. The tooltip renders at the top: 0, left: 0.      │    │
│  │  2. The DOM is mutated.                              │    │
│  │  3. The browser PAINTS the tooltip at the 0, 0.      │    │
│  │     ← the user SEES the wrong position               │    │
│  │  4. The useEffect runs, measures, and setState.      │    │
│  │  5. The re-render moves the tooltip.                 │    │
│  │  6. The browser paints the correct position.         │    │
│  │     ← the user sees the JUMP                         │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  WITH the useLayoutEffect:                                   │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  1. The tooltip renders at the top: 0, left: 0.      │    │
│  │  2. The DOM is mutated.                              │    │
│  │  3. The useLayoutEffect runs, measures, and setState.│    │
│  │  4. The synchronous re-render moves the tooltip.     │    │
│  │  5. The browser PAINTS the correct position.         │    │
│  │     ← the user sees ONLY the correct position        │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  The useLayoutEffect eliminates the flicker.                 │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The SSR Warning

┌──────────────────────────────────────────────────────────────┐
│  THE SSR WARNING                                             │
│                                                              │
│  THE SERVER:                                                 │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  The component renders to a string.                  │    │
│  │  There is NO DOM.                                    │    │
│  │                                                       │    │
│  │  The useLayoutEffect cannot run.                     │    │
│  │  The warning:                                        │    │
│  │  "useLayoutEffect does nothing on the server,        │    │
│  │   because its effect cannot be encoded into the      │    │
│  │   server renderer's output format."                  │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  THE CLIENT:                                                 │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  The DOM exists.                                     │    │
│  │  The useLayoutEffect runs normally.                  │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  THE FIX:                                                    │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  const useIsomorphicLayoutEffect =                   │    │
│  │    typeof window !== 'undefined'                     │    │
│  │      ? useLayoutEffect                               │    │
│  │      : useEffect;                                    │    │
│  │                                                       │    │
│  │  The client uses the layout effect, and the server   │    │
│  │  uses the passive effect.                            │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Layout Thrashing

┌──────────────────────────────────────────────────────────────┐
│  THE LAYOUT THRASHING                                        │
│                                                              │
│  ❌ THE INTERLEAVED:                                          │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  refs.forEach((r) => {                               │    │
│  │    const h = r.offsetHeight;  // the READ             │    │
│  │    r.style.height = `${h}px`; // the WRITE            │    │
│  │  });                                                 │    │
│  │                                                       │    │
│  │  The browser recomputes the layout on every read     │    │
│  │  after the write. The forced reflow.                 │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  ✅ THE BATCHED:                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  // The reads first.                                 │    │
│  │  const heights = refs.map((r) => r.offsetHeight);    │    │
│  │                                                       │    │
│  │  // The writes after.                                │    │
│  │  refs.forEach((r, i) => {                            │    │
│  │    r.style.height = `${heights[i]}px`;               │    │
│  │  });                                                 │    │
│  │                                                       │    │
│  │  The browser computes the layout once.               │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
useLayoutEffectThe synchronous effect before the paint
useEffectThe asynchronous effect after the paint
The timingThe render → the DOM mutation → the layout effect → the paint → the passive effect
The blockingThe useLayoutEffect blocks the paint
The DOM readThe valid in both
The setStateThe synchronous re-render in the layout
The SSRThe warning for the layout
The useIsomorphicLayoutEffectThe client/server switch
The measurementThe layout effect
The data fetchingThe passive effect

Key takeaways:

  • useLayoutEffect runs synchronously after the DOM is mutated but before the browser paints. It is the last chance to read the DOM and change the output before the user sees it. The useEffect runs asynchronously after the paint .
  • The useLayoutEffect eliminates the flicker. The tooltip that measures its position, the list that scrolls to the bottom, and the animation that starts from the computed state are the cases. The useEffect would let the intermediate state paint, and the user would see the jump .
  • The useLayoutEffect blocks the paint. The browser waits for the effect to finish. A slow layout effect — the heavy measurement, the layout thrashing — makes the page feel sluggish. The useLayoutEffect should be the small and the fast .
  • The useLayoutEffect can trigger the synchronous re-render. The setState in the layout effect re-renders the component before the paint, and the user sees only the final state. The useEffect‘s setState re-renders after the paint, and the intermediate state is visible .
  • The useLayoutEffect produces the SSR warning. The server has no DOM, and the effect cannot run. The useIsomorphicLayoutEffect selects the useLayoutEffect on the client and the useEffect on the server .
  • The layout thrashing is the anti-pattern. The interleaved DOM read and write forces the browser to recompute the layout on every read. The fix is to batch the reads first and the writes after .
  • The rule of thumb is to use the useEffect by default. The useLayoutEffect is for the effects that read the DOM and update the state based on the read. The data fetching, the subscriptions, and the logging are the passive effects .
  • The profiler is the tool. The React DevTools Profiler shows the render and the commit timings. The useLayoutEffect that takes the long time is the paint block, and the optimization is the move to the useEffect or the reduction of the work .

Remember: useLayoutEffect is the synchronous hook for the pre-paint DOM reads. It runs after the DOM mutation and before the paint, it can measure the element and update the state, and the user never sees the intermediate state. The useEffect is the asynchronous hook for the post-paint work, and it is the default. The useLayoutEffect blocks the paint, and it should be the small and the fast. The useIsomorphicLayoutEffect handles the SSR. The result is the flicker-free rendering, which is the foundation of the polished UI.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!