useLayoutEffect & Synchronous Effects
MeduseLayoutEffect 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