DEV Community

Maxim Logunov
Maxim Logunov

Posted on

Advanced React Refs: Mastering the Callback Pattern

In the world of React, refs are a powerful escape hatch that allow you to access DOM elements or React component instances directly. While most developers are familiar with the basic useRef hook (const ref = useRef(null)), its more advanced cousin, the ref callback, offers a level of flexibility and control that is essential for complex scenarios. This article will explore what a ref callback is, how it works, and the practical problems it can solve.

What is a Ref Callback?

A ref callback is a function that React will call when a component mounts, passing the DOM element (or class component instance) as its argument. Similarly, it will call the function with null when the component unmounts.

Instead of assigning the ref.current property for you (as useRef does), you handle the node yourself.

Basic Syntax:

function MyComponent() {
  const handleRef = (node) => {
    if (node) {
      // Node is mounted. Do something with the DOM element.
      console.log('Element mounted:', node);
      node.focus(); // Example: focus the input on mount
    } else {
      // Node is unmounted. Clean up if necessary.
      console.log('Element unmounted');
    }
  };

  return <input ref={handleRef} />;
}
Enter fullscreen mode Exit fullscreen mode

Contrast this with the standard useRef approach:

function MyComponent() {
  const inputRef = useRef(null);

  useEffect(() => {
    // Now we need an effect to access the ref after mount.
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []);

  return <input ref={inputRef} />;
}
Enter fullscreen mode Exit fullscreen mode

While the useRef + useEffect pattern is common and perfectly valid for many cases, the ref callback approach is more direct for certain operations on mount.

Key Use Cases and Benefits

1. Dynamic Ref Handling for Lists

This is the most compelling reason to use a ref callback. Imagine you need to store refs for each item in a dynamically generated list. Using a standard useRef would require you to create an array of refs and manage them manually. A ref callback simplifies this immensely.

Example: Managing a list of refs

function DynamicList({ items }) {
  // Store our DOM nodes in a state array
  const [itemNodes, setItemNodes] = useState([]);

  const handleItemRef = (node) => {
    if (node) {
      // Add the node to our array when it's mounted
      setItemNodes((prevNodes) => [...prevNodes, node]);
    }
    // Note: In a real app, you'd also need a cleanup to remove nodes on unmount.
    // This can be done with a separate effect or a more complex ref callback.
  };

  // Now you can programmatically interact with any list item
  const scrollToIndex = (index) => {
    itemNodes[index]?.scrollIntoView();
  };

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id} ref={handleItemRef}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

2. Abstracting Reusable Logic with Custom Hooks

Ref callbacks are the foundation for building powerful custom hooks that need to interact with the DOM. A hook can take a ref callback from its parent and merge it with its own internal ref logic.

Example: A custom useFocus hook

function useFocus(forwardedRef) {
  const innerRef = useRef(null);

  // This is the ref callback we'll return and assign to the component.
  const refCallback = useCallback((node) => {
    // Handle the internal ref
    innerRef.current = node;

    // Handle the forwarded ref (could be a function or a ref object)
    if (typeof forwardedRef === 'function') {
      forwardedRef(node);
    } else if (forwardedRef) {
      forwardedRef.current = node;
    }

    // Our hook's logic: focus the element when it mounts
    if (node) {
      node.focus();
    }
  }, [forwardedRef]); // Re-create the callback if the forwardedRef changes

  return refCallback;
}

// Usage in a component
function MyInput(props) {
  const myRef = useRef(null);
  const inputRef = useFocus(myRef); // Can also forward a ref from a parent

  return <input ref={inputRef} />;
}
Enter fullscreen mode Exit fullscreen mode

This pattern of "ref forwarding" and merging is how libraries like react-aria or framer-motion manage to control DOM elements while still allowing you to pass your own refs.

3. Triggering Logic Directly on Mount/Unmount

Sometimes, you want to perform an action the moment a DOM node is available, without waiting for the useEffect to run after the render is committed to the screen. A ref callback fires synchronously during the render phase when the node is created.

function VideoPlayer({ src }) {
  const handleVideoRef = (node) => {
    if (node) {
      console.log('Video element is ready!');
      node.load(); // Load the video immediately
    } else {
      console.log('Video element is leaving the DOM.');
      // Clean up resources if needed
    }
  };

  return <video ref={handleVideoRef} src={src} controls />;
}
Enter fullscreen mode Exit fullscreen mode

Important Caveats and Best Practices

  1. Closures and Callback Dependencies: The ref callback is called on every render (if the ref itself changes). However, React will call it with null from the previous render and then with the current node. Be cautious of stale closures. If your callback depends on props or state, wrap it in useCallback to avoid unnecessary executions and cleanups.

    const handleRef = useCallback((node) => {
      if (node) {
        // Do something that depends on `someProp`
        node.style.color = someProp;
      }
    }, [someProp]); // Re-create the callback only when `someProp` changes
    
  2. Avoiding Side Effects During Render: While ref callbacks are called during render, you should avoid setting state synchronously within them. This can lead to unpredictable behavior and infinite loops. If you need to set state based on a ref, use the else branch or a useEffect for the null case to handle cleanup.

Conclusion

The ref callback is a versatile tool that moves you from simply holding a DOM reference to reacting to its lifecycle. It's indispensable for:

  • Managing dynamic lists of refs.
  • Building composable custom hooks that need DOM access.
  • Executing code directly the moment a DOM node is created or destroyed.

While useRef(null) is sufficient for most simple cases, understanding and leveraging the ref callback pattern will undoubtedly elevate your ability to write clean, powerful, and flexible React code that interacts directly with the DOM.

Top comments (0)