fix(Link): memoize merged ref to prevent unnecessary ref callback invocations#14853
fix(Link): memoize merged ref to prevent unnecessary ref callback invocations#14853wataryooou wants to merge 2 commits intoremix-run:mainfrom
Conversation
…ocations Wrap `mergeRefs(forwardedRef, prefetchRef)` in `React.useMemo` so that the merged ref callback identity is stable across re-renders. Without this, React treats every render as a ref change, calling the old ref with `null` and the new ref with the DOM node — even when the consumer passes a stable ref callback. Fixes remix-run#12705
|
Hi @wataryooou, Welcome, and thank you for contributing to React Router! Before we consider your pull request, we ask that you sign our Contributor License Agreement (CLA). We require this only once. You may review the CLA and sign it by adding your name to contributors.yml. Once the CLA is signed, the If you have already signed the CLA and received this response in error, or if you have any questions, please contact us at hello@remix.run. Thanks! - The Remix team |
🦋 Changeset detectedLatest commit: f988a43 The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Thank you for signing the Contributor License Agreement. Let's get this merged! 🥳 |
timdorr
left a comment
There was a problem hiding this comment.
As long as the tests pass, looks good to me.
LinkcallsmergeRefs(forwardedRef, prefetchRef)inline in JSX, so a newRefCallbackis created on every render. React sees the ref identity change and calls the old ref withnullthen the new ref with the DOM node — even if the consumer passes a stable ref callback. This causes the ref to be invoked twice on every re-render (#12705).Fixed by wrapping in
useMemo:useMemofits here since we're memoizing the return value ofmergeRefs, not a user callback.prefetchRefcomes fromuseRefso it's stable, andforwardedRefonly changes when the parent passes a different ref — somergedRefstays the same across normal re-renders.Added two tests: one for a plain state-driven re-render, and one for
prefetch="intent"with mouseenter/mouseleave triggering re-renders viaFrameworkContext.Fixes #12705