Skip to main content
useLayoutEffect is identical to useEffect, but it fires synchronously after all DOM mutations and before the browser has a chance to paint. Use this to read layout from the DOM and synchronously re-render.

Signature

function useLayoutEffect(
  effect: () => void | (() => void),
  inputs?: ReadonlyArray<unknown>
): void

Parameters

effect
() => void | (() => void)
required
A function containing imperative, possibly effectful code. Can optionally return a cleanup function that will be called before the effect runs again or when the component unmounts.
inputs
ReadonlyArray<unknown>
An array of dependencies. The effect will only re-run if one of the values in this array has changed (compared using ===). If omitted, the effect runs after every render.

Returns

void

Basic Usage

import { useLayoutEffect, useRef, useState } from 'preact/hooks';

function MeasureElement() {
  const ref = useRef();
  const [height, setHeight] = useState(0);

  useLayoutEffect(() => {
    // Measure DOM element synchronously before paint
    if (ref.current) {
      setHeight(ref.current.offsetHeight);
    }
  }, []);

  return (
    <div>
      <div ref={ref}>Content to measure</div>
      <p>Height: {height}px</p>
    </div>
  );
}

Preventing Flash of Incorrect Content

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

  useLayoutEffect(() => {
    const target = document.getElementById(targetId);
    const tooltip = tooltipRef.current;

    if (target && tooltip) {
      const targetRect = target.getBoundingClientRect();
      const tooltipRect = tooltip.getBoundingClientRect();

      // Calculate position before paint to avoid flicker
      setPosition({
        top: targetRect.bottom + 8,
        left: targetRect.left + (targetRect.width - tooltipRect.width) / 2
      });
    }
  }, [targetId]);

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

Synchronizing with DOM Mutations

function ScrollToBottom({ messages }) {
  const containerRef = useRef();

  useLayoutEffect(() => {
    // Scroll immediately after DOM update, before paint
    if (containerRef.current) {
      containerRef.current.scrollTop = containerRef.current.scrollHeight;
    }
  }, [messages]); // Re-run when messages change

  return (
    <div ref={containerRef} style={{ height: '400px', overflow: 'auto' }}>
      {messages.map(msg => (
        <div key={msg.id}>{msg.text}</div>
      ))}
    </div>
  );
}

Reading Layout Properties

function AdaptiveComponent() {
  const ref = useRef();
  const [isTooWide, setIsTooWide] = useState(false);

  useLayoutEffect(() => {
    if (ref.current) {
      const width = ref.current.getBoundingClientRect().width;
      setIsTooWide(width > 600);
    }
  });

  return (
    <div ref={ref} className={isTooWide ? 'compact' : 'expanded'}>
      Content that adapts to its width
    </div>
  );
}

Focus Management

function AutoFocusInput({ shouldFocus }) {
  const inputRef = useRef();

  useLayoutEffect(() => {
    if (shouldFocus && inputRef.current) {
      // Focus before paint to avoid visible delay
      inputRef.current.focus();
    }
  }, [shouldFocus]);

  return <input ref={inputRef} type="text" />;
}

Measuring and Positioning

function DynamicDropdown({ isOpen, children }) {
  const dropdownRef = useRef();
  const [style, setStyle] = useState({});

  useLayoutEffect(() => {
    if (isOpen && dropdownRef.current) {
      const rect = dropdownRef.current.getBoundingClientRect();
      const viewportHeight = window.innerHeight;

      // Determine if dropdown should open upward or downward
      const shouldOpenUpward = rect.bottom + 200 > viewportHeight;

      setStyle({
        position: 'absolute',
        [shouldOpenUpward ? 'bottom' : 'top']: '100%',
        left: 0,
        width: '100%'
      });
    }
  }, [isOpen]);

  return (
    <div ref={dropdownRef}>
      {isOpen && (
        <div style={style} className="dropdown-menu">
          {children}
        </div>
      )}
    </div>
  );
}
Performance Warning: useLayoutEffect runs synchronously and blocks the browser from painting. This can hurt performance if used excessively. Prefer useEffect when possible.
Use useLayoutEffect when you need to:
  • Read layout from the DOM (measurements, positions)
  • Mutate the DOM synchronously before paint
  • Prevent visual flicker or “flash of incorrect content”
Updates scheduled inside useLayoutEffect will be flushed synchronously, after all DOM mutations but before the browser paints.

Build docs developers (and LLMs) love