react-merge-refs icon indicating copy to clipboard operation
react-merge-refs copied to clipboard

Feature proposal: `useMergeRefs` that tracks refs individually

Open alvaro-cuesta opened this issue 4 months ago • 2 comments

🚀 Feature Proposal

useMergeRefs that tracks refs individually instead of relying on useMemo.

Motivation

This reproduction explains the issue.

Even if useMergeRef can make stable refs under some circunstances, as soon as we're dealing with external refs the stability is poisoned and the guarantees explained in React's ref callback documentation are not upheld.

I know this is probably inconsequential for the 80% use-case, but there are legitimate use cases in tracking mounts/unmounts with refs (or doing expensive setup/cleanup that we'd like to avoid on every re-render) that is completely broken (and worse: impossible to fix!) by not having stable merged refs.

I know useEffect or useSyncExternalStore covers some... but not all of those cases.

I'd go further: my proposal is to do a breaking change in useMergeRefs API:

  • An array does not cut it for proper stability guarantees and is footgun-y.
  • It would be better to track refs by having them keyed (i.e. use an object).
  • Think e.g. someone handling a collection of refs (adding and removing refs) while an object would be a foolproof API for them (give a "key" to every ref).
  • The problem (and only solution) is very similar to React's motivation for having key prop.
  • On the consumer side it's not that different, except it prevents footgun-y operations.

In fact I'd seriously consider completely deprecating mergeRefs, maybe add a warning, rename it to something like unsafe_mergeRefs, or maybe just delete it (in a future version). Since it poisons any refs I might want to behave stably by someone unknowingly using it deep in a component I don't control (and vice-versa) I think it should be considered unsafe.

Example

const Example = ({ externalRef }) => {
  const internalRef = useRef(null);

  return <div
    ref={useMergeRefs({ externalRef, internalRef })}
  />
};

Pitch

I think this belongs in this package because it's the only package that I found that does the right thing for React 19... except for this minor detail.

I think it's important for the API to make it easy to uphold React's guarantees even in complex scenarios and for people that are not aware of the fine details of ref lifecycles. Having this in a popular package like this one will make the whole React ecosystem better in the long run.

alvaro-cuesta avatar Aug 22 '25 18:08 alvaro-cuesta