ReactuseLayoutEffect Hook

useLayoutEffect Hook

useLayoutEffect is a close sibling of useEffect — it takes the same signature and behaves almost identically. The one critical difference is when it runs: useLayoutEffect fires synchronously after React has updated the DOM but before the browser has painted the screen. This tiny timing window is exactly what you need to read DOM measurements or make synchronous DOM corrections without the user ever seeing the intermediate state.

The Timing Difference

useEffect

useLayoutEffect

When it fires

Async — after browser paint

Sync — before browser paint

Blocks paint?

No — browser paints first

Yes — paint waits for the effect

Can read/write layout without flicker?

No — flash of wrong layout

Yes — user never sees intermediate state

Server-side rendering

Runs normally

Skipped (no DOM on server)

Performance impact

Minimal

Blocks visual update — be fast

Why the Difference Matters: The Flicker Problem

When you need to read a DOM measurement and immediately update something based on it, using useEffect causes a visible flicker: the browser paints the initial position first, then your effect runs and moves things, causing a jarring jump. useLayoutEffect prevents this by running before the paint.

JSX
import { useState, useEffect, useRef } from 'react'

// ❌ With useEffect — tooltip position flashes at wrong location
function Tooltip({ targetRef, text }) {
  const tooltipRef = useRef(null)
  const [style, setStyle] = useState({})

  useEffect(() => {
    const target = targetRef.current.getBoundingClientRect()
    const tooltip = tooltipRef.current.getBoundingClientRect()

    // This runs AFTER paint — user briefly sees tooltip at default position
    setStyle({
      top: target.top - tooltip.height - 8,
      left: target.left + (target.width - tooltip.width) / 2,
    })
  })

  return (
    <div
      ref={tooltipRef}
      style={{ position: 'fixed', ...style }}
    >
      {text}
    </div>
  )
}

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

// ✅ With useLayoutEffect — tooltip is positioned before the user sees it
function Tooltip({ targetRef, text }) {
  const tooltipRef = useRef(null)
  const [style, setStyle] = useState({ visibility: 'hidden' })

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

    // Runs BEFORE paint — DOM is updated but user hasn't seen anything yet
    setStyle({
      visibility: 'visible',
      position: 'fixed',
      top: target.top - tooltip.height - 8,
      left: target.left + (target.width - tooltip.width) / 2,
    })
  })

  return (
    <div ref={tooltipRef} style={style}>
      {text}
    </div>
  )
}
Use Case 1: Reading DOM Layout Before Paint

Any time you need to measure a DOM element and respond to that measurement in the same visual frame, use useLayoutEffect.

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

function AutoResizeTextarea({ value }) {
  const ref = useRef(null)

  useLayoutEffect(() => {
    // Reset height so it can shrink, then grow to fit content
    ref.current.style.height = 'auto'
    ref.current.style.height = ref.current.scrollHeight + 'px'
    // User never sees 'auto' — this runs before paint
  }, [value])

  return (
    <textarea
      ref={ref}
      value={value}
      readOnly
      style={{ overflow: 'hidden', resize: 'none' }}
    />
  )
}
Use Case 2: Positioning Popovers and Dropdowns

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

function Dropdown({ trigger, children }) {
  const [open, setOpen] = useState(false)
  const triggerRef = useRef(null)
  const menuRef = useRef(null)
  const [menuStyle, setMenuStyle] = useState({})

  useLayoutEffect(() => {
    if (!open || !triggerRef.current || !menuRef.current) return

    const triggerRect = triggerRef.current.getBoundingClientRect()
    const menuRect = menuRef.current.getBoundingClientRect()
    const viewportHeight = window.innerHeight

    const spaceBelow = viewportHeight - triggerRect.bottom
    const spaceAbove = triggerRect.top

    // Open above if not enough room below
    const openAbove = spaceBelow < menuRect.height && spaceAbove > spaceBelow

    setMenuStyle({
      position: 'fixed',
      left: triggerRect.left,
      top: openAbove
        ? triggerRect.top - menuRect.height - 4
        : triggerRect.bottom + 4,
      width: triggerRect.width,
    })
  }, [open])

  return (
    <div>
      <button ref={triggerRef} onClick={() => setOpen(o => !o)}>
        {trigger}
      </button>
      {open && (
        <ul ref={menuRef} style={menuStyle}>
          {children}
        </ul>
      )}
    </div>
  )
}
Use Case 3: Integrating Third-Party DOM Libraries

Some libraries (charting libraries, canvas-based tools, legacy jQuery plugins) expect the DOM to be ready and want to control it directly. useLayoutEffect ensures you initialize them after the DOM is updated but before the user sees the page.

JSX
import { useLayoutEffect, useRef } from 'react'

function ChartContainer({ data }) {
  const containerRef = useRef(null)
  const chartInstance = useRef(null)

  useLayoutEffect(() => {
    // Initialize the chart synchronously — user never sees an un-initialized container
    chartInstance.current = ThirdPartyChart.create(containerRef.current, {
      data,
      width: containerRef.current.clientWidth,
      height: 300,
    })

    return () => {
      // Cleanup: destroy the chart on unmount
      chartInstance.current?.destroy()
    }
  }, []) // initialize once

  useLayoutEffect(() => {
    // Update the chart data synchronously whenever 'data' changes
    chartInstance.current?.setData(data)
  }, [data])

  return <div ref={containerRef} />
}
Server-Side Rendering: A Critical Caveat

On the server there is no DOM — useLayoutEffect has nowhere to run. React will skip it entirely and emit a warning in development:

Note
⚠️ Warning: useLayoutEffect does nothing on the server because its effect cannot be encoded into the server renderer's output format.

Two ways to handle this:

JSX
// Option 1: Guard with a typeof window check
// Good for effects that genuinely only make sense client-side
import { useEffect, useLayoutEffect } from 'react'

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

// Then use useIsomorphicLayoutEffect in your component:
function Component() {
  useIsomorphicLayoutEffect(() => {
    // Runs as useLayoutEffect on client, useEffect on server (no-op)
  }, [])
}

// Option 2: Use a 'mounted' guard to skip on first SSR pass
function Component() {
  const [isMounted, setIsMounted] = useState(false)

  useEffect(() => {
    setIsMounted(true)
  }, [])

  useLayoutEffect(() => {
    if (!isMounted) return
    // safe DOM measurement
  }, [isMounted])
}
Performance Warning
Warning
`useLayoutEffect` blocks the browser from painting. If your effect is slow — doing a lot of computation, making synchronous network requests, or causing multiple layout reflows — the user will see a frozen/janky UI. Keep layout effects fast (measure, update a ref, set a small piece of state) and defer everything else to `useEffect`.
useEffect vs useLayoutEffect: The Decision
  • Default choice: useEffect — async, does not block painting, works on server

  • Use useLayoutEffect when: you must read DOM measurements (size, position) and immediately make a visual correction that would flicker with useEffect

  • Rule of thumb: if removing useLayoutEffect and replacing with useEffect causes a visible flash or flicker, then useLayoutEffect was the right choice

Tip
Most applications have zero or one use of `useLayoutEffect` — typically in a low-level tooltip or popover component. If you find yourself reaching for it frequently, consider whether a CSS-only solution (absolute positioning within a relative parent, CSS transforms) could solve the layout problem without JavaScript at all.