Understanding the differences between these two hooks is crucial for optimizing performance and avoiding visual glitches in React applications.
Key Differences
Feature | useEffect | useLayoutEffect |
---|---|---|
Timing | After browser paint | Before browser paint |
Use Case | Side effects | DOM measurements/synchronization |
Performance | Better for most cases | Can block rendering |
Visual Impact | May cause flickering | Prevents visual inconsistencies |
Server Rendering | Runs on both server and client | Warning on server |
When to Use useEffect
useEffect
is the appropriate choice for most side effects and should be your default choice.
Common Use Cases:
- Data fetching
- Subscriptions
- Manual DOM mutations that don’t affect layout
- Setting timers
- Logging/analytics
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// Correct - data fetching doesn't need layout effects
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
return <div>{user?.name}</div>;
}
When to Use useLayoutEffect
Use useLayoutEffect
when you need to read or mutate the DOM and want the changes to be synchronous with React’s rendering.
Common Use Cases:
- DOM measurements (element sizes, positions)
- Synchronously updating DOM before paint
- Animations that depend on layout
- Preventing visual “flickering”
function Tooltip({ children, targetRef }) {
const [position, setPosition] = useState({ top: 0, left: 0 });
useLayoutEffect(() => {
if (targetRef.current) {
const rect = targetRef.current.getBoundingClientRect();
// Calculate position before browser paints
setPosition({
top: rect.bottom + window.scrollY,
left: rect.left + window.scrollX
});
}
}, [targetRef]);
return (
<div style={{ position: 'absolute', ...position }}>
{children}
</div>
);
}
Performance Implications
useEffect
Behavior:
- Component renders
- Browser paints the screen
useEffect
runs- If state changes, another render occurs
- Browser repaints
useLayoutEffect
Behavior:
- Component renders
useLayoutEffect
runs- If state changes, React re-renders synchronously
- Browser paints once with final state
function Example() {
const [width, setWidth] = useState(0);
const ref = useRef();
// This would cause a flicker with useEffect
useLayoutEffect(() => {
setWidth(ref.current.offsetWidth);
}, []);
return <div ref={ref}>Width: {width}px</div>;
}
Practical Examples
1. Auto-Resizing Textarea (requires useLayoutEffect
)
function AutoResizeTextarea({ value }) {
const textareaRef = useRef();
useLayoutEffect(() => {
// Reset height to get correct scrollHeight
textareaRef.current.style.height = 'auto';
// Set new height based on content
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
}, [value]);
return (
<textarea
ref={textareaRef}
value={value}
style={{ resize: 'none', overflow: 'hidden' }}
/>
);
}
2. Animation Starting Point (requires useLayoutEffect
)
function AnimatedBox() {
const boxRef = useRef();
useLayoutEffect(() => {
// Get initial position before any animation
const rect = boxRef.current.getBoundingClientRect();
boxRef.current.style.transform = `translateX(${rect.width}px)`;
// Trigger animation
requestAnimationFrame(() => {
boxRef.current.style.transition = 'transform 0.5s ease';
boxRef.current.style.transform = 'translateX(0)';
});
}, []);
return <div ref={boxRef} className="box" />;
}
3. Scroll Position Restoration (better with useLayoutEffect
)
function ScrollRestorer({ key }) {
const scrollPositions = useRef({});
useLayoutEffect(() => {
const scrollY = scrollPositions.current[key];
if (scrollY !== undefined) {
window.scrollTo(0, scrollY);
}
return () => {
scrollPositions.current[key] = window.scrollY;
};
}, [key]);
return null;
}
Server-Side Rendering (SSR) Considerations
useLayoutEffect
will warn when used in SSR because:
- There’s no DOM to measure on the server
- The effect can’t run before the first paint (which happens on the client)
Solution: Either:
- Use
useEffect
instead if possible - Dynamically detect environment and use the appropriate hook
// Solution for SSR compatibility
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
function SSRSafeComponent() {
useIsomorphicLayoutEffect(() => {
// DOM measurement code
}, []);
return /* ... */;
}
Debugging Tips
- Visual Flickering: If you see visual changes after initial render, consider
useLayoutEffect
- Performance Issues: If UI feels sluggish, check for unnecessary
useLayoutEffect
usage - Infinite Loops: Both hooks can cause loops if state updates aren’t properly guarded
// Bad - infinite loop potential
useLayoutEffect(() => {
setState(calculate(state)); // Might trigger re-render
}, [state]);
// Better
useLayoutEffect(() => {
const newState = calculate(state);
if (newState !== state) {
setState(newState);
}
}, [state]);
Best Practices
- Default to
useEffect
: It’s the safer choice for most cases - Measure First: Use the React DevTools profiler to identify real issues
- Minimize
useLayoutEffect
: Only use when visual synchronization is critical - Combine with
useRef
: For storing measurements without causing re-renders - SSR Awareness: Be mindful of server rendering implications
function OptimizedComponent() {
const [size, setSize] = useState({ width: 0, height: 0 });
const ref = useRef();
// Only runs when ref changes, not on every render
useLayoutEffect(() => {
if (ref.current) {
setSize({
width: ref.current.offsetWidth,
height: ref.current.offsetHeight
});
}
}, [ref.current]); // Ref object identity is stable
return <div ref={ref}>Size: {size.width}x{size.height}</div>;
}
Remember that while useLayoutEffect
can solve certain visual problems, overusing it can harm performance. Always verify that you actually need the synchronous behavior before reaching for it.