Skip to main content
useMergedRef merges multiple refs (callback refs or ref objects) into one. Pass the returned ref to an element to keep all refs in sync.

Usage

import { useMergedRef } from '@kivora/react';
import { useRef, forwardRef } from 'react';

const MyComponent = forwardRef<HTMLDivElement, Props>((props, forwardedRef) => {
  const localRef = useRef<HTMLDivElement>(null);
  const mergedRef = useMergedRef(localRef, forwardedRef);

  return <div ref={mergedRef}>Content</div>;
});

Parameters

...refs
Array<React.Ref<T> | undefined | null>
required
Variable number of refs to merge. Can be ref objects (React.RefObject), callback refs, or undefined/null (which are ignored).

Returns

React.RefCallback<T> - A memoized callback ref that updates all provided refs.

Examples

Forwarding Refs with Local Ref

const FancyInput = forwardRef<HTMLInputElement, InputProps>(
  (props, forwardedRef) => {
    const inputRef = useRef<HTMLInputElement>(null);
    const mergedRef = useMergedRef(inputRef, forwardedRef);

    const focus = () => {
      inputRef.current?.focus();
    };

    return (
      <div>
        <input ref={mergedRef} {...props} />
        <button onClick={focus}>Focus</button>
      </div>
    );
  }
);

Multiple Callback Refs

function TrackedElement() {
  const observerRef = useCallback((node: HTMLDivElement | null) => {
    if (node) {
      const observer = new IntersectionObserver(handleIntersection);
      observer.observe(node);
    }
  }, []);

  const resizeRef = useCallback((node: HTMLDivElement | null) => {
    if (node) {
      const observer = new ResizeObserver(handleResize);
      observer.observe(node);
    }
  }, []);

  const mergedRef = useMergedRef(observerRef, resizeRef);

  return <div ref={mergedRef}>Tracked content</div>;
}

Combining with Third-Party Libraries

import { useFloating } from '@floating-ui/react';

const Tooltip = forwardRef<HTMLButtonElement, TooltipProps>(
  ({ children }, forwardedRef) => {
    const { refs } = useFloating();
    const mergedRef = useMergedRef(refs.setReference, forwardedRef);

    return <button ref={mergedRef}>{children}</button>;
  }
);

Multiple Ref Objects

function MeasuredComponent() {
  const ref1 = useRef<HTMLDivElement>(null);
  const ref2 = useRef<HTMLDivElement>(null);
  const ref3 = useRef<HTMLDivElement>(null);

  const mergedRef = useMergedRef(ref1, ref2, ref3);

  useEffect(() => {
    // All refs point to the same element
    console.log(ref1.current === ref2.current); // true
    console.log(ref2.current === ref3.current); // true
  }, []);

  return <div ref={mergedRef}>Content</div>;
}

With Optional Refs

const FlexibleComponent = forwardRef<HTMLDivElement, Props>(
  ({ enableTracking }, forwardedRef) => {
    const localRef = useRef<HTMLDivElement>(null);
    const trackingRef = enableTracking ? useTrackingRef() : null;

    // useMergedRef handles null/undefined refs gracefully
    const mergedRef = useMergedRef(localRef, forwardedRef, trackingRef);

    return <div ref={mergedRef}>Content</div>;
  }
);

Complex Forwarding Pattern

const Form = forwardRef<HTMLFormElement, FormProps>(
  ({ onSubmit }, forwardedRef) => {
    const formRef = useRef<HTMLFormElement>(null);

    const callbackRef = useCallback((node: HTMLFormElement | null) => {
      if (node) {
        // Attach custom validation
        node.addEventListener('invalid', handleInvalid, true);
      }
    }, []);

    const mergedRef = useMergedRef(formRef, forwardedRef, callbackRef);

    return (
      <form ref={mergedRef} onSubmit={onSubmit}>
        {/* form fields */}
      </form>
    );
  }
);

Notes

  • The returned callback ref is memoized and updates when the provided refs change
  • Handles both ref objects ({ current: T }) and callback refs automatically
  • Ignores undefined and null refs, making it safe to use with optional refs
  • Particularly useful when building reusable components that need to forward refs while maintaining internal refs
  • All provided refs will point to the same element instance

Build docs developers (and LLMs) love