How to Optimize useRef Hook in React

The useRef hook in React is a powerful tool for persisting values across renders without triggering a re-render and for directly accessing DOM elements. This article provides a practical guide on how to optimize your use of useRef, covering lazy initialization of expensive values, avoiding rendering phase side effects, and preventing memory leaks through proper cleanup.

1. Implement Lazy Initialization for Expensive Objects

When you pass an initial value directly to useRef(), React executes that initial value expression on every single render, even though the ref’s value is only set once during the initial mount.

For example, this instantiates a new class on every render:

// Unoptimized: Expensive object is created on every render
const myRef = useRef(new ExpensiveClass());

To optimize this and ensure the expensive object is only created once, use lazy initialization by checking if the ref is null:

// Optimized: Expensive object is created only once
const myRef = useRef(null);

if (myRef.current === null) {
  myRef.current = new ExpensiveClass();
}

2. Avoid Reading or Writing ref.current During Render

React expects components to behave like pure functions with respect to rendering. Reading or writing to useRef during the render phase can lead to unpredictable behavior, especially in React’s Concurrent Mode where renders can be aborted or retried.

3. Use useRef to Prevent Unnecessary Re-renders

Use useRef instead of useState for any data that does not affect what is displayed on the screen. Updating state triggers a component re-render, whereas updating a ref does not.

Common use cases for optimization include storing: * Timer or interval IDs * Tracking variables (e.g., whether a component has mounted) * Storing previous prop values for comparison

function Timer() {
  // Storing the interval ID in a ref prevents re-renders when the interval starts/stops
  const intervalRef = useRef(null);

  const startTimer = () => {
    if (intervalRef.current !== null) return;
    intervalRef.current = setInterval(() => {
      // timer logic
    }, 1000);
  };

  const stopTimer = () => {
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  };

  return (
    <div>
      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
    </div>
  );
}

4. Properly Clean Up Refs to Prevent Memory Leaks

If you use useRef to hold third-party library instances, timers, or WebSocket connections, always clean them up when the component unmounts. Failing to do so can cause memory leaks.

useEffect(() => {
  const socket = new WebSocket('ws://example.com');
  socketRef.current = socket;

  return () => {
    // Cleanup on unmount
    if (socketRef.current) {
      socketRef.current.close();
    }
  };
}, []);