My last post talked about creating refs in React.  There are times when working with React, however, that you might need to assign two refs to the same Component or element.  Why?

A simple use case is for a component like a Tooltip.

Our Tooltip component can wrap either plain text or another component that acts as the trigger for the tooltip.  Meaning, when the user hovers over the trigger element or component, our tool tip should display; and when the user moves the mouse outside the trigger element, our tool tip should hide.

Because our Tooltip could wrap any other component as the trigger, we want to ensure we preserve the ref, if any, passed to this trigger.  But, our tooltip also needs to add its own ref to the trigger element so we can dynamically position our Tooltip centered directly underneath the triggering element.  

Here's how it would work in context:

import Tooltip from 'Tooltip';

ReactDOM.render(
  <p>
    This sentence has a {" "}
    <Tooltip text="hey there!">tooltip trigger</Tooltip>{" "}
    in it that can be hovered.
  </p>,
  document.getElementById('root')
);

Essentially, you have a component you're building that needs a local ref on some element; but you also want to allow the parent to pass a ref as well; and maintain both refs on a single element.

Here's the mergeRefs() helper method we'll be using. It accepts any number of refs to merge and returns a single callback ref that will properly set each ref passed, whether that ref is a function or a ref object.

const mergeRefs = (...refs) => {
  const filteredRefs = refs.filter(Boolean);
  if (!filteredRefs.length) return null;
  if (filteredRefs.length === 0) return filteredRefs[0];
  return inst => {
    for (const ref of filteredRefs) {
      if (typeof ref === 'function') {
        ref(inst);
      } else if (ref) {
        ref.current = inst;
      }
    }
  };
};

And here's our Tooltip implementation for reference.  We only worry about displaying the tooltip below the trigger element to keep things simple.

const Tooltip = ({ text, children }) => {
  const [active, setActive] = React.useState(false);
  const [{ top, left }, setPosition] = React.useState({ top: 0, left: 0 });
  // local reference on trigger element
  const trigger = React.useRef(null);
  // local reference on tooltip element
  const tip = React.useRef(null);
  
  // mouse event handlers
  const show = () => setActive(true);
  const hide = () => setActive(false);
  
  // allow using <Tooltip> to wrap any
  // React node, as well as plain text.
  const child = typeof children === 'string' ? 
        <span>{children}</span> :
        React.Children.only(children);
  
  // anytime we show (activate) the tooltip, we'll
  // dynamically calculate the position relative to
  // the current location of the trigger element
  React.useEffect(() => {
    if (active && (trigger && trigger.current) && (tip && tip.current)) {
      const triggerEl = trigger.current.getBoundingClientRect();
      const tipEl = tip.current.getBoundingClientRect();
      setPosition({ 
        top: (triggerEl.y + window.pageYOffset) + triggerEl.height, 
        left: (triggerEl.x + window.pageXOffset) + ((triggerEl.width - tipEl.width)/2) 
      });
    }
  }, [active, trigger, tip]);
  
  // We use our mergeRefs() helper to preserve any ref
  // already on the trigger element, as well as our own ref.
  return (
    <>
    { React.cloneElement(child, {
        ...child.props,
        ref: mergeRefs(child.ref, trigger),
        onMouseEnter: child.props['onMouseEnter'] ? 
          composeCallbacks(child.props.onMouseEnter, show) : 
          show,
        onMouseLeave: child.props['onMouseLeave'] ? 
          composeCallbacks(child.props.onMouseLeave, hide) : 
          hide
      })}  
    {/* 
      Use React's createPortal() to ensure our tool tip
      is always above other elements and can breakout of
      popups, etc. if necessary. And only render it if we're
      active (hovering)
    */}
    { active && ReactDOM.createPortal(
        <div 
          className="tooltip" 
          ref={tip} 
          style={{ position: 'absolute', top, left }}
          >
          {text}
        </div>,
        document.body)
    }
    </>
  );
}

You can see the full working example in the following Codepen:

This technique is also useful if you're writing a component library and your component maintains local refs to internal elements for behavior purposes; but wants to allow the user of that component to also pass along a ref as well.  We allow our component user to pass us a regular ref by using React.forwardRef() on our library component's implementation.

const Button = React.forwardRef(
  ({ children, onClick }, ref) => {
    const local = React.useRef(null);
    
    return (
      <button 
        className="button" 
        type="button"
        ref={mergeRefs(local, ref)}
        onClick={onClick}
        >
        {children}
      </button>
    );
  }
);

Honestly, the cases where  you would need to use a helper function like mergeRefs are not all that common, unless you're writing a component library or components meant to be reused by others.  I struggled to come up with some good examples that couldn't be better solved in some other fashion.  If you've got some good examples from your own experience, I'd love to hear about them in the comments!