useLayoutEffect & Synchronous Effects

Med

useLayoutEffect fires synchronously after DOM mutations but before the browser paints, allowing you to read layout and apply changes without visual flicker. It has the same signature as useEffect but different timing, making it essential for DOM measurements and position calculations.

Interactive Visualization

Key Points

  • Same API as useEffect but runs synchronously after DOM mutation, before paint
  • Use for DOM measurements that must happen before the user sees the frame
  • Prevents visual flicker when reading and immediately writing to the DOM
  • Blocks painting, so overuse degrades performance
  • Emits a warning during SSR because the server has no DOM to measure
  • The vast majority of effects should use useEffect instead

Code Examples

Tooltip Positioning

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

interface TooltipProps {
  targetRef: React.RefObject<HTMLElement | null>
  text: string
}

function Tooltip({ targetRef, text }: TooltipProps) {
  const tooltipRef = useRef<HTMLDivElement>(null)
  const [position, setPosition] = useState({ top: 0, left: 0 })

  useLayoutEffect(() => {
    if (!targetRef.current || !tooltipRef.current) return
    const rect = targetRef.current.getBoundingClientRect()
    const tooltipRect = tooltipRef.current.getBoundingClientRect()

    setPosition({
      top: rect.bottom + 8,
      left: rect.left + (rect.width - tooltipRect.width) / 2,
    })
  }, [targetRef, text])

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

useLayoutEffect measures the target element and positions the tooltip before the browser paints, preventing the tooltip from flashing at the wrong position

Auto-scrolling Container

import { useLayoutEffect, useRef } from 'react'

function ChatMessages({ messages }: { messages: string[] }) {
  const containerRef = useRef<HTMLDivElement>(null)

  useLayoutEffect(() => {
    const el = containerRef.current
    if (el) {
      el.scrollTop = el.scrollHeight
    }
  }, [messages])

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

Scrolling happens synchronously before paint so the user never sees the old scroll position when new messages arrive

Common Mistakes

  • Using useLayoutEffect for all effects instead of only when flicker prevention is needed
  • Performing expensive work inside useLayoutEffect that blocks rendering
  • Using useLayoutEffect in SSR code where it causes hydration warnings

Interview Tips

  • Draw the timeline: render -> DOM mutation -> useLayoutEffect -> paint -> useEffect
  • Give concrete examples where useEffect causes flicker and useLayoutEffect fixes it
  • Know the SSR workaround: check typeof window or use useEffect with isomorphic fallback

Related Concepts