Back to Blog
ReactHooksuseLayoutEffectDOM

React useLayoutEffect Guide

Understand when to use useLayoutEffect vs useEffect for DOM measurements and synchronous updates.

B
Bootspring Team
Engineering
August 28, 2018
7 min read

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 runs

When 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.

Share this article

Help spread the word about Bootspring