Skip to content

fix(Link): memoize merged ref to prevent unnecessary ref callback invocations#14853

Open
wataryooou wants to merge 2 commits intoremix-run:mainfrom
wataryooou:fix/link-memoize-merged-ref
Open

fix(Link): memoize merged ref to prevent unnecessary ref callback invocations#14853
wataryooou wants to merge 2 commits intoremix-run:mainfrom
wataryooou:fix/link-memoize-merged-ref

Conversation

@wataryooou
Copy link

Link calls mergeRefs(forwardedRef, prefetchRef) inline in JSX, so a new RefCallback is created on every render. React sees the ref identity change and calls the old ref with null then 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:

let mergedRef = React.useMemo(
  () => mergeRefs(forwardedRef, prefetchRef),
  [forwardedRef, prefetchRef],
);

useMemo fits here since we're memoizing the return value of mergeRefs, not a user callback. prefetchRef comes from useRef so it's stable, and forwardedRef only changes when the parent passes a different ref — so mergedRef stays 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 via FrameworkContext.

Fixes #12705

…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
@remix-cla-bot
Copy link
Contributor

remix-cla-bot bot commented Mar 5, 2026

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 CLA Signed label will be added to the pull request.

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-bot
Copy link

changeset-bot bot commented Mar 5, 2026

🦋 Changeset detected

Latest commit: f988a43

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
react-router Patch
@react-router/architect Patch
@react-router/cloudflare Patch
@react-router/dev Patch
react-router-dom Patch
@react-router/express Patch
@react-router/node Patch
@react-router/serve Patch
@react-router/fs-routes Patch
@react-router/remix-routes-option-adapter Patch
create-react-router Patch

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

@remix-cla-bot
Copy link
Contributor

remix-cla-bot bot commented Mar 5, 2026

Thank you for signing the Contributor License Agreement. Let's get this merged! 🥳

Copy link
Member

@timdorr timdorr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as the tests pass, looks good to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stable callback fn passed as ref to a Link gets called twice on every re-render

2 participants