useLayoutEffect vs. useEffect – When to Use Each

Loading

Understanding the differences between these two hooks is crucial for optimizing performance and avoiding visual glitches in React applications.

Key Differences

FeatureuseEffectuseLayoutEffect
TimingAfter browser paintBefore browser paint
Use CaseSide effectsDOM measurements/synchronization
PerformanceBetter for most casesCan block rendering
Visual ImpactMay cause flickeringPrevents visual inconsistencies
Server RenderingRuns on both server and clientWarning on server

When to Use useEffect

useEffect is the appropriate choice for most side effects and should be your default choice.

Common Use Cases:

  1. Data fetching
  2. Subscriptions
  3. Manual DOM mutations that don’t affect layout
  4. Setting timers
  5. 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:

  1. DOM measurements (element sizes, positions)
  2. Synchronously updating DOM before paint
  3. Animations that depend on layout
  4. 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:

  1. Component renders
  2. Browser paints the screen
  3. useEffect runs
  4. If state changes, another render occurs
  5. Browser repaints

useLayoutEffect Behavior:

  1. Component renders
  2. useLayoutEffect runs
  3. If state changes, React re-renders synchronously
  4. 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:

  1. There’s no DOM to measure on the server
  2. The effect can’t run before the first paint (which happens on the client)

Solution: Either:

  1. Use useEffect instead if possible
  2. 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

  1. Visual Flickering: If you see visual changes after initial render, consider useLayoutEffect
  2. Performance Issues: If UI feels sluggish, check for unnecessary useLayoutEffect usage
  3. 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

  1. Default to useEffect: It’s the safer choice for most cases
  2. Measure First: Use the React DevTools profiler to identify real issues
  3. Minimize useLayoutEffect: Only use when visual synchronization is critical
  4. Combine with useRef: For storing measurements without causing re-renders
  5. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *