Skip to content

Stabilize all composed refs#3968

Open
chaance wants to merge 5 commits into
mainfrom
chance/stable-refs
Open

Stabilize all composed refs#3968
chaance wants to merge 5 commits into
mainfrom
chance/stable-refs

Conversation

@chaance

@chaance chaance commented Jun 15, 2026

Copy link
Copy Markdown
Member

Filling in the gaps in #3967 and adding test coverage.

@changeset-bot

changeset-bot Bot commented Jun 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7f1c396

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

This PR includes changesets to release 19 packages
Name Type
@radix-ui/react-dismissable-layer Patch
@radix-ui/react-dropdown-menu Patch
@radix-ui/react-navigation-menu Patch
@radix-ui/react-focus-scope Patch
@radix-ui/react-scroll-area Patch
@radix-ui/react-select Patch
@radix-ui/react-popper Patch
@radix-ui/react-slider Patch
@radix-ui/react-toast Patch
@radix-ui/react-menu Patch
@radix-ui/react-use-escape-keydown Patch
@radix-ui/react-dialog Patch
@radix-ui/react-hover-card Patch
@radix-ui/react-popover Patch
radix-ui Patch
@radix-ui/react-tooltip Patch
@radix-ui/react-context-menu Patch
@radix-ui/react-menubar Patch
@radix-ui/react-alert-dialog 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

@petrarca

Copy link
Copy Markdown

Thanks for this work — #3967 and this PR fix the known call sites, but useComposedRefs itself still has the unstable identity:

function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
  // eslint-disable-next-line react-hooks/exhaustive-deps
  return React.useCallback(composeRefs(...refs), refs);
}

The refs spread creates a new array on every render, so the useCallback dep check always fails and the composed ref gets a new identity every commit. Any caller that passes an inline ref or a state setter not already stabilized via useCallbackRef will reproduce the infinite loop.

We've been running this fix in production across Tooltip, Select, Popover and Dialog with React 19.2:

function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
  const refsRef = React.useRef(refs);
  refsRef.current = refs;
  return React.useCallback((node: T) => composeRefs(...refsRef.current)(node), []);
}

The refsRef holds the latest refs without being a dep, so the callback identity is stable across renders. Would be happy to add this to the PR if it fits the direction you're going.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants