When building React applications, you might encounter situations where components briefly flicker or show unwanted visual states. This often happens when measuring and updating DOM elements. Let’s explore how to fix this using useLayoutEffect
.
The Problem: UI Flicker
Here’s a common scenario where useEffect
causes flickering:
const ResponsiveNavigation = () => {
const [visibleItems, setVisibleItems] = useState(links.length);
const containerRef = useRef(null);
useEffect(() => {
const calculateVisibleItems = () => {
const container = containerRef.current;
const containerWidth = container.offsetWidth;
const itemWidth = 100; // Example fixed width
const newVisibleItems = Math.floor(containerWidth / itemWidth);
setVisibleItems(newVisibleItems);
};
calculateVisibleItems();
window.addEventListener('resize', calculateVisibleItems);
return () => window.removeEventListener('resize', calculateVisibleItems);
}, []);
return (
<nav ref={containerRef}>
{links.slice(0, visibleItems).map(link => (
<Link key={link.id} {...link} />
))}
{visibleItems < links.length && <MoreButton />}
</nav>
);
};
This code will cause a flash where all links are shown before being cut down to the correct number.
The Solution: useLayoutEffect
Let’s fix it by replacing useEffect
with useLayoutEffect
:
const ResponsiveNavigation = () => {
const [visibleItems, setVisibleItems] = useState(0); // Start with 0 instead
const containerRef = useRef(null);
useLayoutEffect(() => {
const calculateVisibleItems = () => {
const container = containerRef.current;
const containerWidth = container.offsetWidth;
const itemWidth = 100;
const newVisibleItems = Math.floor(containerWidth / itemWidth);
setVisibleItems(newVisibleItems);
};
calculateVisibleItems();
window.addEventListener('resize', calculateVisibleItems);
return () => window.removeEventListener('resize', calculateVisibleItems);
}, []);
return (
<nav ref={containerRef}>
{links.slice(0, visibleItems).map(link => (
<Link key={link.id} {...link} />
))}
{visibleItems < links.length && <MoreButton />}
</nav>
);
};
Understanding the Difference
The key differences between useEffect
and useLayoutEffect
:
// useEffect: Runs asynchronously after render
useEffect(() => {
// Browser has already painted
// Visual changes here can cause flicker
}, []);
// useLayoutEffect: Runs synchronously before browser paint
useLayoutEffect(() => {
// Browser hasn't painted yet
// Visual changes here won't cause flicker
}, []);
Real World Example: Tooltip Positioning
Here’s a practical example of a tooltip that requires precise positioning:
const Tooltip = ({ text, targetRef }) => {
const [position, setPosition] = useState({ top: 0, left: 0 });
const tooltipRef = useRef(null);
useLayoutEffect(() => {
if (!targetRef.current || !tooltipRef.current) return;
const targetRect = targetRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
setPosition({
top: targetRect.top - tooltipRect.height - 10,
left: targetRect.left + (targetRect.width - tooltipRect.width) / 2
});
}, [targetRef]);
return (
<div
ref={tooltipRef}
style={{
position: 'fixed',
top: position.top,
left: position.left
}}
>
{text}
</div>
);
};
Performance Considerations
useLayoutEffect
runs synchronously, which means it can block visual updates. Use it only when necessary:
// ❌ Don't use useLayoutEffect for data fetching
const Component = () => {
useLayoutEffect(() => {
fetchData(); // Bad! Will block rendering
}, []);
};
// ✅ Use useEffect for operations that don't need DOM measurements
const Component = () => {
useEffect(() => {
fetchData(); // Good! Won't block rendering
}, []);
};
Server-Side Rendering Concerns
useLayoutEffect
doesn’t work during SSR. Here’s a pattern to handle this:
const useSafeLayoutEffect = () => {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const Component = () => {
useLayoutEffect(() => {
if (!mounted) return;
// DOM measurements and updates here
}, [mounted]);
if (!mounted) {
return <Placeholder />; // Show a placeholder during SSR
}
return <ActualContent />;
};
};
Key Takeaways
- Use
useLayoutEffect
when you need to measure and update DOM elements - Use
useEffect
for everything else - Be careful with
useLayoutEffect
in SSR applications - Consider performance implications -
useLayoutEffect
is synchronous