fix(slot): memoize composed ref in SlotClone to prevent infinite loop…#3835
Open
ishantmehta01 wants to merge 4 commits intoradix-ui:mainfrom
Open
fix(slot): memoize composed ref in SlotClone to prevent infinite loop…#3835ishantmehta01 wants to merge 4 commits intoradix-ui:mainfrom
ishantmehta01 wants to merge 4 commits intoradix-ui:mainfrom
Conversation
… in React 19 Fixes radix-ui#3799 ## Problem In `SlotClone`, `composeRefs(forwardedRef, childrenRef)` is called inline during render, creating a new function identity on every render. In React 19, ref identity changes trigger ref cleanup + re-attach. When a state setter is used as a ref (e.g., Tooltip's `setTrigger`), this causes an infinite loop: 1. New `composeRefs` → React detects ref identity change 2. React calls cleanup (`setTrigger(null)`) → state update → re-render 3. Re-render creates another new `composeRefs` → goto 1 4. "Maximum update depth exceeded" ## Solution Memoize the composed ref via `React.useMemo` so the ref identity stays stable across renders. The memoization depends on `forwardedRef` and `childrenRef` — it only recomputes when the actual refs change, not on every render. ## How this differs from radix-ui#3804 PR radix-ui#3804 switches to `useComposedRefs`, but comments there note that `useComposedRefs` itself is unstable when callers pass inline arrow functions. This approach memoizes directly at the call site in `SlotClone`, which is the minimal change needed — it keeps `composeRefs` untouched and just stabilizes the ref identity where it matters. ## Validation Running in production on a large enterprise React 19 app since Feb 2026. Zero recurrence of the infinite loop across Tooltip, Select, Popover, and Dialog components.
🦋 Changeset detectedLatest commit: 8e5438e The changes in this PR will be included in the next version bump. 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
… in React 19
Fixes #3799
Problem
In
SlotClone,composeRefs(forwardedRef, childrenRef)is called inline during render, creating a new function identity on every render. In React 19, ref identity changestrigger ref cleanup + re-attach. When a state setter is used as a ref (e.g., Tooltip's
setTrigger), this causes an infinite loop:composeRefs→ React detects ref identity changesetTrigger(null)) → state update → re-rendercomposeRefs→ goto 1Solution
Memoize the composed ref via
React.useMemoso the ref identity stays stable across renders. The memoization depends onforwardedRefandchildrenRef— it only recomputeswhen the actual refs change, not on every render.
How this differs from #3804
PR #3804 switches to
useComposedRefs, but comments there note thatuseComposedRefsitself is unstable when callers pass inline arrow functions. This approach memoizesdirectly at the call site in
SlotClone, which is the minimal change needed — it keepscomposeRefsuntouched and just stabilizes the ref identity where it matters.Validation
Running in production on a large enterprise React 19 app since Feb 2026. Zero recurrence of the infinite loop across Tooltip, Select, Popover, and Dialog components.
Description