What's the difference between useEffect and useLayoutEffect? When would you use each?

3 minintermediatereactdifferenceuseeffectuselayouteffect

Quick Answer

useEffect and useLayoutEffect are both hooks for handling side effects, but they differ in when they execute during the React rendering cycle.

Detailed Answer

What's the difference between useEffect and useLayoutEffect? When would you use each?

Answer:

useEffect and useLayoutEffect are both hooks for handling side effects, but they differ in when they execute during the React rendering cycle.

Key Differences:

  1. Timing:

    • useEffect: Runs after the DOM has been updated and painted to the screen (asynchronous)
    • useLayoutEffect: Runs synchronously after all DOM mutations but before the browser paints
  2. Performance Impact:

    • useEffect: Non-blocking, doesn't delay visual updates
    • useLayoutEffect: Blocking, can delay visual updates if it takes too long

When to Use useEffect (Most Common):

function DataFetcher({ userId }: { userId: string }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);

  // Good for data fetching, subscriptions, timers
  useEffect(() => {
    setLoading(true);
    fetchUser(userId)
      .then(setUser)
      .finally(() => setLoading(false));
  }, [userId]);

  // Good for cleanup
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('Timer tick');
    }, 1000);

    return () => clearInterval(timer);
  }, []);

  return loading ? <div>Loading...</div> : <div>{user?.name}</div>;
}

When to Use useLayoutEffect (Specific Cases):

function Tooltip({ children, content }: { children: React.ReactNode; content: string }) {
  const [position, setPosition] = useState({ top: 0, left: 0 });
  const tooltipRef = useRef<HTMLDivElement>(null);
  const triggerRef = useRef<HTMLDivElement>(null);

  // Use useLayoutEffect to prevent visual flicker
  useLayoutEffect(() => {
    if (tooltipRef.current && triggerRef.current) {
      const triggerRect = triggerRef.current.getBoundingClientRect();
      const tooltipRect = tooltipRef.current.getBoundingClientRect();
      
      // Calculate position to prevent tooltip from going off-screen
      const top = triggerRect.bottom + 8;
      const left = Math.max(8, Math.min(
        triggerRect.left,
        window.innerWidth - tooltipRect.width - 8
      ));
      
      setPosition({ top, left });
    }
  }, [content]);

  return (
    <div ref={triggerRef} style={{ position: 'relative' }}>
      {children}
      <div
        ref={tooltipRef}
        style={{
          position: 'absolute',
          top: position.top,
          left: position.left,
          background: 'black',
          color: 'white',
          padding: '4px 8px',
          borderRadius: '4px',
          zIndex: 1000,
        }}
      >
        {content}
      </div>
    </div>
  );
}

// Another example: Measuring DOM elements
function DynamicHeightComponent({ content }: { content: string }) {
  const [height, setHeight] = useState(0);
  const contentRef = useRef<HTMLDivElement>(null);

  useLayoutEffect(() => {
    if (contentRef.current) {
      // Measure the actual rendered height
      const measuredHeight = contentRef.current.scrollHeight;
      setHeight(measuredHeight);
    }
  }, [content]);

  return (
    <div style={{ height: height || 'auto' }}>
      <div ref={contentRef}>{content}</div>
    </div>
  );
}

Summary:

  • Use useEffect for 99% of side effects (data fetching, subscriptions, cleanup)
  • Use useLayoutEffect only when you need to read layout from the DOM and synchronously re-render to prevent visual flicker
  • Common use cases for useLayoutEffect: tooltips, modals, measuring elements, animations that depend on DOM measurements