useLayoutEffect runs synchronously after DOM mutations but before the browser paints. Here's when and how to use it.
useEffect vs useLayoutEffect#
1import { useEffect, useLayoutEffect, useRef, useState } from 'react';
2
3function Comparison() {
4 const [count, setCount] = useState(0);
5
6 // Runs AFTER browser paint (async)
7 useEffect(() => {
8 console.log('useEffect - after paint');
9 }, [count]);
10
11 // Runs BEFORE browser paint (sync)
12 useLayoutEffect(() => {
13 console.log('useLayoutEffect - before paint');
14 }, [count]);
15
16 return <div>{count}</div>;
17}
18
19// Execution order:
20// 1. Render
21// 2. DOM updated
22// 3. useLayoutEffect runs (blocks paint)
23// 4. Browser paints
24// 5. useEffect runsWhen to Use useLayoutEffect#
1// 1. DOM measurements
2function Tooltip({ target, children }) {
3 const tooltipRef = useRef(null);
4 const [position, setPosition] = useState({ top: 0, left: 0 });
5
6 useLayoutEffect(() => {
7 const targetRect = target.getBoundingClientRect();
8 const tooltipRect = tooltipRef.current.getBoundingClientRect();
9
10 setPosition({
11 top: targetRect.bottom + 8,
12 left: targetRect.left + (targetRect.width - tooltipRect.width) / 2
13 });
14 }, [target]);
15
16 return (
17 <div
18 ref={tooltipRef}
19 style={{
20 position: 'absolute',
21 top: position.top,
22 left: position.left
23 }}
24 >
25 {children}
26 </div>
27 );
28}
29
30// 2. Prevent visual flicker
31function FlickerFree() {
32 const [height, setHeight] = useState(0);
33 const ref = useRef(null);
34
35 // ❌ useEffect: element might flash at wrong size
36 // ✓ useLayoutEffect: size is correct before paint
37 useLayoutEffect(() => {
38 setHeight(ref.current.getBoundingClientRect().height);
39 }, []);
40
41 return <div ref={ref}>Content with calculated height: {height}</div>;
42}DOM Measurements#
1function AutoResizeTextarea({ value, onChange }) {
2 const textareaRef = useRef(null);
3
4 useLayoutEffect(() => {
5 const textarea = textareaRef.current;
6 // Reset height to auto to get scroll height
7 textarea.style.height = 'auto';
8 // Set to scroll height
9 textarea.style.height = `${textarea.scrollHeight}px`;
10 }, [value]);
11
12 return (
13 <textarea
14 ref={textareaRef}
15 value={value}
16 onChange={onChange}
17 style={{ overflow: 'hidden', resize: 'none' }}
18 />
19 );
20}
21
22// Measure element after content change
23function MeasuredContent({ children }) {
24 const ref = useRef(null);
25 const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
26
27 useLayoutEffect(() => {
28 const { width, height } = ref.current.getBoundingClientRect();
29 setDimensions({ width, height });
30 }, [children]);
31
32 return (
33 <div>
34 <div ref={ref}>{children}</div>
35 <p>Size: {dimensions.width}x{dimensions.height}</p>
36 </div>
37 );
38}Scroll Position#
1function ScrollToBottom({ messages }) {
2 const containerRef = useRef(null);
3
4 // Scroll before paint to prevent flash
5 useLayoutEffect(() => {
6 containerRef.current.scrollTop = containerRef.current.scrollHeight;
7 }, [messages]);
8
9 return (
10 <div ref={containerRef} style={{ height: '400px', overflow: 'auto' }}>
11 {messages.map((msg, i) => (
12 <div key={i}>{msg}</div>
13 ))}
14 </div>
15 );
16}
17
18// Preserve scroll position during updates
19function PreserveScroll({ items }) {
20 const containerRef = useRef(null);
21 const scrollRef = useRef(0);
22
23 // Capture scroll before update
24 useLayoutEffect(() => {
25 const container = containerRef.current;
26 const prevScrollHeight = container.scrollHeight;
27
28 return () => {
29 scrollRef.current = container.scrollHeight - prevScrollHeight;
30 };
31 });
32
33 // Restore scroll after update
34 useLayoutEffect(() => {
35 containerRef.current.scrollTop += scrollRef.current;
36 }, [items]);
37
38 return <div ref={containerRef}>{/* ... */}</div>;
39}Animation Initialization#
1function AnimatedElement({ show }) {
2 const ref = useRef(null);
3
4 useLayoutEffect(() => {
5 if (show) {
6 // Set initial state before paint
7 ref.current.style.opacity = '0';
8 ref.current.style.transform = 'translateY(20px)';
9
10 // Trigger reflow
11 ref.current.offsetHeight;
12
13 // Set final state
14 ref.current.style.transition = 'all 0.3s ease';
15 ref.current.style.opacity = '1';
16 ref.current.style.transform = 'translateY(0)';
17 }
18 }, [show]);
19
20 if (!show) return null;
21
22 return <div ref={ref}>Animated content</div>;
23}
24
25// Prevent FOUC (Flash of Unstyled Content)
26function PreventFOUC({ theme }) {
27 const ref = useRef(null);
28
29 useLayoutEffect(() => {
30 // Apply theme before paint
31 ref.current.className = `theme-${theme}`;
32 }, [theme]);
33
34 return <div ref={ref}>Themed content</div>;
35}Focus Management#
1function AutoFocusInput({ shouldFocus }) {
2 const inputRef = useRef(null);
3
4 useLayoutEffect(() => {
5 if (shouldFocus) {
6 inputRef.current.focus();
7 }
8 }, [shouldFocus]);
9
10 return <input ref={inputRef} />;
11}
12
13// Focus first invalid field
14function Form({ errors }) {
15 const formRef = useRef(null);
16
17 useLayoutEffect(() => {
18 if (Object.keys(errors).length > 0) {
19 const firstErrorField = formRef.current.querySelector('[data-error="true"]');
20 if (firstErrorField) {
21 firstErrorField.focus();
22 }
23 }
24 }, [errors]);
25
26 return (
27 <form ref={formRef}>
28 <input data-error={!!errors.name} />
29 <input data-error={!!errors.email} />
30 </form>
31 );
32}Canvas and WebGL#
1function CanvasChart({ data }) {
2 const canvasRef = useRef(null);
3
4 useLayoutEffect(() => {
5 const canvas = canvasRef.current;
6 const ctx = canvas.getContext('2d');
7
8 // Clear and draw before paint
9 ctx.clearRect(0, 0, canvas.width, canvas.height);
10
11 // Draw chart
12 data.forEach((point, i) => {
13 ctx.fillRect(i * 10, canvas.height - point, 8, point);
14 });
15 }, [data]);
16
17 return <canvas ref={canvasRef} width={300} height={200} />;
18}
19
20// WebGL setup
21function WebGLScene() {
22 const canvasRef = useRef(null);
23
24 useLayoutEffect(() => {
25 const canvas = canvasRef.current;
26 const gl = canvas.getContext('webgl');
27
28 // Setup must complete before paint
29 gl.clearColor(0.0, 0.0, 0.0, 1.0);
30 gl.clear(gl.COLOR_BUFFER_BIT);
31
32 // ... more WebGL setup
33 }, []);
34
35 return <canvas ref={canvasRef} />;
36}Third-Party Libraries#
1function ChartComponent({ data }) {
2 const containerRef = useRef(null);
3 const chartRef = useRef(null);
4
5 useLayoutEffect(() => {
6 // Initialize chart library before paint
7 chartRef.current = new Chart(containerRef.current, {
8 type: 'bar',
9 data: data
10 });
11
12 return () => {
13 chartRef.current.destroy();
14 };
15 }, []);
16
17 // Update chart data synchronously
18 useLayoutEffect(() => {
19 if (chartRef.current) {
20 chartRef.current.data = data;
21 chartRef.current.update();
22 }
23 }, [data]);
24
25 return <div ref={containerRef} />;
26}
27
28// D3 integration
29function D3Chart({ data }) {
30 const svgRef = useRef(null);
31
32 useLayoutEffect(() => {
33 const svg = d3.select(svgRef.current);
34
35 // D3 manipulation before paint
36 svg.selectAll('rect')
37 .data(data)
38 .join('rect')
39 .attr('x', (d, i) => i * 20)
40 .attr('y', d => 100 - d)
41 .attr('width', 15)
42 .attr('height', d => d);
43 }, [data]);
44
45 return <svg ref={svgRef} width={200} height={100} />;
46}Server-Side Rendering#
1// useLayoutEffect warning in SSR
2import { useEffect, useLayoutEffect } from 'react';
3
4// Create isomorphic version
5const useIsomorphicLayoutEffect =
6 typeof window !== 'undefined' ? useLayoutEffect : useEffect;
7
8function Component() {
9 const ref = useRef(null);
10
11 // Works on both server and client
12 useIsomorphicLayoutEffect(() => {
13 if (ref.current) {
14 // DOM manipulation
15 }
16 }, []);
17
18 return <div ref={ref}>Content</div>;
19}
20
21// Or suppress the warning
22function ClientOnlyComponent() {
23 const [mounted, setMounted] = useState(false);
24
25 useEffect(() => {
26 setMounted(true);
27 }, []);
28
29 if (!mounted) return null;
30
31 // Now safe to use useLayoutEffect
32 return <ComponentWithLayoutEffect />;
33}Performance Considerations#
1// ❌ Don't use for async operations
2useLayoutEffect(() => {
3 // This blocks the browser!
4 fetch('/api/data'); // Don't do this
5}, []);
6
7// ✓ Use useEffect for async
8useEffect(() => {
9 fetch('/api/data').then(setData);
10}, []);
11
12// ❌ Don't use for expensive calculations
13useLayoutEffect(() => {
14 // This blocks the browser!
15 const result = expensiveCalculation(data);
16}, [data]);
17
18// ✓ Use useMemo for calculations
19const result = useMemo(() => expensiveCalculation(data), [data]);
20
21// ✓ Only use useLayoutEffect for DOM operations
22useLayoutEffect(() => {
23 // Quick DOM measurement/update
24 ref.current.style.height = `${ref.current.scrollHeight}px`;
25}, [content]);Custom Hooks with useLayoutEffect#
1// useDimensions hook
2function useDimensions(ref) {
3 const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
4
5 useLayoutEffect(() => {
6 if (ref.current) {
7 const { width, height } = ref.current.getBoundingClientRect();
8 setDimensions({ width, height });
9 }
10 }, [ref]);
11
12 return dimensions;
13}
14
15// useLockBodyScroll hook
16function useLockBodyScroll() {
17 useLayoutEffect(() => {
18 const originalStyle = window.getComputedStyle(document.body).overflow;
19 document.body.style.overflow = 'hidden';
20
21 return () => {
22 document.body.style.overflow = originalStyle;
23 };
24 }, []);
25}
26
27// useScrollPosition hook
28function useScrollPosition() {
29 const [scroll, setScroll] = useState({ x: 0, y: 0 });
30
31 useLayoutEffect(() => {
32 const handleScroll = () => {
33 setScroll({ x: window.scrollX, y: window.scrollY });
34 };
35
36 window.addEventListener('scroll', handleScroll, { passive: true });
37 return () => window.removeEventListener('scroll', handleScroll);
38 }, []);
39
40 return scroll;
41}Best Practices#
When to Use useLayoutEffect:
✓ DOM measurements
✓ Scroll position management
✓ Preventing visual flicker
✓ Synchronous animations
✓ Third-party DOM libraries
When to Use useEffect:
✓ Data fetching
✓ Subscriptions
✓ Logging
✓ Any async operation
✓ Most side effects
Performance:
✓ Keep useLayoutEffect fast
✓ Avoid expensive computations
✓ Don't block with async code
✓ Prefer useEffect when possible
SSR:
✓ Use isomorphic version
✓ Handle client-only cases
✓ Suppress warnings appropriately
Conclusion#
useLayoutEffect runs synchronously before the browser paints, making it essential for DOM measurements and preventing visual flicker. Use it sparingly for synchronous DOM operations like measuring elements, managing scroll position, or initializing animations. For most side effects, useEffect is the better choice as it doesn't block the browser. Remember to handle SSR cases with an isomorphic version.