patternjavascriptreactModerate
useLayoutEffect for DOM measurements — runs synchronously before paint
Viewed 0 times
React 16.8+
useLayoutEffectuseEffectDOM measurementflickerpaintgetBoundingClientRectisomorphic
browser
Error Messages
Problem
useEffect runs after the browser has painted. Reading DOM measurements (offsetWidth, getBoundingClientRect) inside useEffect and immediately setting state based on them causes a visible flash: the browser paints the first state, then React re-renders with the measured state, painting again. Users see a flicker.
Solution
Use useLayoutEffect for DOM measurements that must happen before paint:
import { useLayoutEffect, useRef, useState } from 'react';
// Tooltip that positions itself to stay in the viewport
function Tooltip({ children, label }) {
const ref = useRef(null);
const [tooltipLeft, setTooltipLeft] = useState(0);
// Runs synchronously after DOM mutation but before paint
useLayoutEffect(() => {
const rect = ref.current.getBoundingClientRect();
const overflowRight = rect.right - window.innerWidth;
if (overflowRight > 0) {
setTooltipLeft(-overflowRight - 8); // shift left to fit
}
}, []);
return (
<div ref={ref} style={{ left: tooltipLeft }} className="tooltip">
{label}
</div>
);
}
// Rule of thumb:
// useEffect: subscriptions, data fetching, logging — no DOM reads/writes
// useLayoutEffect: DOM measurement → state → avoid flicker
// SSR warning: useLayoutEffect logs a warning on the server
// Use useIsomorphicLayoutEffect pattern for SSR-compatible code:
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
import { useLayoutEffect, useRef, useState } from 'react';
// Tooltip that positions itself to stay in the viewport
function Tooltip({ children, label }) {
const ref = useRef(null);
const [tooltipLeft, setTooltipLeft] = useState(0);
// Runs synchronously after DOM mutation but before paint
useLayoutEffect(() => {
const rect = ref.current.getBoundingClientRect();
const overflowRight = rect.right - window.innerWidth;
if (overflowRight > 0) {
setTooltipLeft(-overflowRight - 8); // shift left to fit
}
}, []);
return (
<div ref={ref} style={{ left: tooltipLeft }} className="tooltip">
{label}
</div>
);
}
// Rule of thumb:
// useEffect: subscriptions, data fetching, logging — no DOM reads/writes
// useLayoutEffect: DOM measurement → state → avoid flicker
// SSR warning: useLayoutEffect logs a warning on the server
// Use useIsomorphicLayoutEffect pattern for SSR-compatible code:
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
Why
React fires useLayoutEffect after all DOM mutations but before the browser paints. This synchronous execution means any state updates triggered inside it are batched with the current commit — the user only sees the final layout. useEffect fires after paint, so a setState inside it causes a second visible paint.
Gotchas
- useLayoutEffect blocks the browser from painting until it completes — keep it fast
- Never fetch data in useLayoutEffect — it blocks paint and provides no benefit over useEffect for async work
- On the server (SSR), useLayoutEffect is a no-op and logs a warning — use the useIsomorphicLayoutEffect pattern
- In React 18 concurrent mode, useLayoutEffect fires synchronously after every committed render, including interrupted renders
Code Snippets
useIsomorphicLayoutEffect for SSR compatibility
// SSR-safe pattern
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
// Usage — same API as useLayoutEffect
function MeasuredBox({ children }) {
const ref = useRef(null);
const [size, setSize] = useState({ width: 0, height: 0 });
useIsomorphicLayoutEffect(() => {
const { width, height } = ref.current.getBoundingClientRect();
setSize({ width, height });
}, []);
return <div ref={ref}>{children}</div>;
}Context
When reading DOM dimensions or positions immediately after render and using them to update layout before the user sees the initial state
Revisions (0)
No revisions yet.