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.
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>
)
}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.
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
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.
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:
Two ways to handle this:
// 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
useEffect vs useLayoutEffect: The Decision
Default choice:
useEffect— async, does not block painting, works on serverUse
useLayoutEffectwhen: you must read DOM measurements (size, position) and immediately make a visual correction that would flicker withuseEffectRule of thumb: if removing
useLayoutEffectand replacing withuseEffectcauses a visible flash or flicker, thenuseLayoutEffectwas the right choice