useLayoutEffect vs useEffect: Eliminating UI Flicker in React

Oscar Bustos

Learn when and how to use useLayoutEffect to prevent visual glitches in your React applications

6 min read

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

  1. Use useLayoutEffect when you need to measure and update DOM elements
  2. Use useEffect for everything else
  3. Be careful with useLayoutEffect in SSR applications
  4. Consider performance implications - useLayoutEffect is synchronous

Is it a match?

If you need help or advice, feel free to drop us a message via social media or email

Contact me