fix(slot): use memoized useComposedRefs in SlotClone to prevent React 19 infinite loops#3804
Open
yannisgu wants to merge 2 commits intoradix-ui:mainfrom
Open
fix(slot): use memoized useComposedRefs in SlotClone to prevent React 19 infinite loops#3804yannisgu wants to merge 2 commits intoradix-ui:mainfrom
yannisgu wants to merge 2 commits intoradix-ui:mainfrom
Conversation
…ite loops SlotClone previously called composeRefs() directly, creating a new ref callback function on every render. In React 19, callback refs can return cleanup functions, and when composeRefs' internal setRef returns ref(value), React treats the composed ref as having cleanup semantics. Combined with a new ref identity every render, React unmounts the old ref and mounts the new one each render cycle. When a state setter is passed as one of the refs (e.g. SelectTrigger's onTriggerChange, or Checkbox's useSize), the ref unmount/remount triggers a state update during React's commit phase. If anything else also triggers a re-render during commit (e.g. @tanstack/react-form's useField layout effect), this creates an infinite synchronous loop that freezes the browser tab. Fix: use useComposedRefs (which wraps composeRefs in React.useCallback) to memoize the composed ref callback, keeping its identity stable across renders and preventing the unmount/remount cycle. Fixes radix-ui#3799
🦋 Changeset detectedLatest commit: 12278ef The changes in this PR will be included in the next version bump. This PR includes changesets to release 44 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 |
hawkeye217
added a commit
to blakeblackshear/frigate
that referenced
this pull request
Mar 5, 2026
- Patch @radix-ui/react-compose-refs@1.1.2: stabilize useComposedRefs to prevent infinite render loops from unstable ref callbacks radix-ui/primitives#3799 - Patch @radix-ui/react-slot@1.2.4: use useComposedRefs hook in SlotClone instead of inline composeRefs to prevent re-render cycles radix-ui/primitives#3804 - Patch react-use-websocket@4.8.1: remove flushSync wrappers that cause "Maximum update depth exceeded" with React 19 auto-batching facebook/react#27613 - Add npm overrides to ensure single hoisted copies of compose-refs and react-slot across all Radix packages - Add postinstall script for patch-package - Remove leftover react-transition-group dependency
11 tasks
NickM-27
pushed a commit
to blakeblackshear/frigate
that referenced
this pull request
Mar 5, 2026
* remove unused RecoilRoot and fix implicit ref callback
Remove the vestigial recoil dependency (zero consumers) and convert
the implicit-return ref callback in SearchView to block form to
prevent React 19 interpreting it as a cleanup function.
* replace react-transition-group with framer-motion in Chip
Replace CSSTransition with framer-motion AnimatePresence + motion.div
for React 19 compatibility (react-transition-group uses findDOMNode).
framer-motion is already a project dependency.
* migrate react-grid-layout v1 to v2
- Replace WidthProvider(Responsive) HOC with useContainerWidth hook
- Update types: Layout (single item) → LayoutItem, Layout[] → Layout
- Replace isDraggable/isResizable/resizeHandles with dragConfig/resizeConfig
- Update EventCallback signature for v2 API
- Remove @types/react-grid-layout (v2 includes its own types)
* upgrade vaul, next-themes, framer-motion, react-zoom-pan-pinch
- vaul: ^0.9.1 → ^1.1.2
- next-themes: ^0.3.0 → ^0.4.6
- framer-motion: ^11.5.4 → ^12.35.0 (React 19 native support)
- react-zoom-pan-pinch: 3.4.4 → latest
* upgrade to React 19, react-konva v19, eslint-plugin-react-hooks v5
Core React 19 upgrade with all necessary type fixes:
- Update RefObject types to accept T | null (React 19 refs always nullable)
- Add JSX namespace imports (no longer global in React 19)
- Add initial values to useRef calls (required in React 19)
- Fix ReactElement.props unknown type in config-form components
- Fix IconWrapper interface to use HTMLAttributes instead of index signature
- Add monaco-editor as dev dependency for type declarations
- Upgrade react-konva to v19, eslint-plugin-react-hooks to v5
* upgrade typescript to 5.9.3
* modernize Context.Provider to React 19 shorthand
Replace <Context.Provider value={...}> with <Context value={...}>
across all project-owned context providers. External library contexts
(react-icons IconContext, radix TooltipPrimitive) left unchanged.
* add runtime patches for React 19 compatibility
- Patch @radix-ui/react-compose-refs@1.1.2: stabilize useComposedRefs
to prevent infinite render loops from unstable ref callbacks
radix-ui/primitives#3799
- Patch @radix-ui/react-slot@1.2.4: use useComposedRefs hook in
SlotClone instead of inline composeRefs to prevent re-render cycles
radix-ui/primitives#3804
- Patch react-use-websocket@4.8.1: remove flushSync wrappers that
cause "Maximum update depth exceeded" with React 19 auto-batching
facebook/react#27613
- Add npm overrides to ensure single hoisted copies of compose-refs
and react-slot across all Radix packages
- Add postinstall script for patch-package
- Remove leftover react-transition-group dependency
* formatting
* use availableWidth instead of useContainerWidth for grid layout
The useContainerWidth hook from react-grid-layout v2 returns raw
container width without accounting for scrollbar width, causing the
grid to not fill the full available space. Use the existing
availableWidth value from useResizeObserver which already compensates
for scrollbar width, matching the working implementation.
* remove unused carousel component and fix React 19 peer deps
Remove embla-carousel-react and its unused Carousel UI component.
Upgrade sonner v1 → v2 for native React 19 support. Remove
@types/react-icons stub (react-icons bundles its own types).
These changes eliminate all peer dependency conflicts, so
npm install works without --legacy-peer-deps.
* fix React 19 infinite re-render loop on live dashboard
The "Maximum update depth exceeded" error was caused by two issues:
1. useDeferredStreamMetadata returned a new `{}` default on every render
when SWR data was undefined, creating an unstable reference that
triggered the useEffect in useCameraLiveMode on every render cycle.
Fixed by using a stable module-level EMPTY_METADATA constant.
2. useResizeObserver's rest parameter `...refs` created a new array on
every render, causing its useEffect to re-run and re-observe elements
continuously. Fixed by stabilizing refs with useRef and only
reconnecting the observer when actual DOM elements change.
shih-ch
added a commit
to shih-ch/buddhist-translator
that referenced
this pull request
Mar 5, 2026
Root cause: Radix UI's SlotClone calls composeRefs() directly during render, creating a new ref identity every render. In React 19, ref callbacks can return cleanup functions, and new ref identity triggers continuous unmount/remount cycles that freeze the browser. Fix: Patch @radix-ui/react-slot to use memoized useComposedRefs() hook instead of raw composeRefs() — stabilizes ref identity across renders. This matches upstream PR radix-ui/primitives#3804. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
need this, thank you! |
ishantmehta01
added a commit
to ishantmehta01/primitives
that referenced
this pull request
Mar 23, 2026
… 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.
NAinfini
added a commit
to NAinfini/Inifni-Lucid-Fin
that referenced
this pull request
Apr 19, 2026
React 19 changed ref-callback semantics: when a composed ref callback's identity changes across renders, React treats it as unmount→mount of the ref, invoking the callback with null then with the node. If any of the composed refs is a useState setter (common inside Radix primitives like Tooltip/Popover/Menu internals), this re-fires the setter on every commit, causing "Maximum update depth exceeded" inside setRef. @radix-ui/react-compose-refs@1.1.2 uses React.useCallback(composeRefs(...refs), refs) but the spread `refs` array is a new array every render, so the dep array is effectively unstable and the callback identity flips every render — triggering the above cycle. @radix-ui/react-slot@1.2.4's SlotClone calls `composeRefs()` directly without any memoization, with the same result. Patched both packages (both .js and .mjs outputs) via patch-package to: - useComposedRefs: store refs in a ref + return a stable [] callback - SlotClone: use useComposedRefs instead of raw composeRefs Matches the approach in upstream PR radix-ui/primitives#3804 (open, not yet released). Removes the crash seen on CanvasToolbar when Tooltip + asChild + forwardRef wraps ToolbarButton.
NAinfini
added a commit
to NAinfini/Inifni-Lucid-Fin
that referenced
this pull request
Apr 19, 2026
…& layout rework (#51) * feat(folders): Phase 1 — contracts + storage for entity folders Introduce first-class Folder entity for character/equipment/location/asset. Each kind gets its own folders table with unlimited nesting via parent_id; entities gain a nullable folder_id column (null = virtual "Uncategorized"). - contracts: Folder / FolderKind types + folderId on 4 entity DTOs - contracts-parse: schemas + typed table cols include folder_id - storage: SCHEMA_SQL adds 4 folders tables + folder_id cols - storage: migration 003 idempotently upgrades existing DBs - storage: FolderRepository with CRUD + cycle prevention + cascade - storage: entity/asset repos read/write folder_id + setFolder helpers All packages typecheck clean. Unit tests written but not executed here (native better-sqlite3 locked by running dev process in this env; rebuild via `npm rebuild better-sqlite3` after stopping dev server). Phase 1 of .trellis/tasks/04-18-entity-folders-and-layout-rework. * feat(folders): Phase 2 — IPC + preload + renderer slices for entity folders Wires the FolderRepository (Phase 1) into the renderer-visible API surface: Main (IPC): - Register `registerFolderHandlers` in router; exposes `folder.<kind>:list/create/rename/move/delete` for kind ∈ {character,equipment,location,asset}, plus `<entity>:setFolder` channels. Preload: - Expose `window.lucidAPI.folder.{character|equipment|location|asset}` with `list/create/rename/move/delete`. - Add `setFolder(id|hash, folderId)` to character/equipment/location/asset API groups. Renderer types: - Declare `FolderKindApi` and the `folder` group on `LucidAPI` in `global.d.ts`; add `setFolder` to the four entity API groups. Redux (state + reducers per slice): - characters/equipment/locations/assets: add `folders: Folder[]`, `currentFolderId: string | null`, `foldersLoading: boolean` to state. - New reducers: `setFolders`, `addFolder`, `updateFolder`, `removeFolder` (also clears stale `folderId` refs on items + resets current folder), `setCurrentFolder`, `setFoldersLoading`, `moveItemToFolder`. - Assets slice: add optional `folderId` to `Asset`, and key `moveItemToFolder` on `hash` to match its storage identity. Tests: - Fixtures in `*.test.ts` (characters/equipment/locations) and `EntityManagerPanels.test.tsx` updated to include the new folder substate fields so `restore(...)` payloads remain type-complete. Verification: desktop-main + desktop-renderer typecheck clean. Runtime tests still blocked by the better-sqlite3 native rebuild lock (dev server holds the file handle) — will run in a clean shell after Phase 3. * feat(folders): Phase 3 — FolderTree, FolderBreadcrumb, useEntityFolders; wire Characters panel Adds the shared folder UI surface (kind-agnostic) and pilots it on the Character manager. Equipment/Location/Asset panels will follow in Phase 4-5 using the same primitives. New: - hooks/useEntityFolders.ts: kind-agnostic hook that wires the preload `folder.<kind>` API to any slice exposing the standard folder action creators. Exposes list/create/rename/move/delete plus a derived `breadcrumb` (root→currentFolder ancestry). - components/canvas/folders/FolderTree.tsx: recursive folder tree with inline create/rename, delete, chevron expand/collapse, hover actions, and drag-drop target via a pluggable `dropItemKey` (default `application/lucid-entity-id`). Callers wire `onDropItem` for moves. - components/canvas/folders/FolderBreadcrumb.tsx: root → current path with per-segment navigation. Character pilot (CharacterManagerPanel.tsx): - Initializes `useEntityFolders({ kind: 'character', ... })`. - Collapsible FolderTree above the existing list/detail grid; FolderBreadcrumb in the header showing current path. - Filters the list by `currentFolderId` (null = all); the search box still narrows within the folder. - `createNewCharacter` places the new row in the current folder via `folderId`, so "New" inside a folder stays inside it. - Items are draggable (sets DND_MIME to character id); FolderTree drop zones invoke `handleMoveCharacterToFolder`, which calls `character.setFolder(id, folderId)` and dispatches `moveItemToFolder` locally. i18n: - Adds `action.rename` and a `folders.*` block (label/all/toggle/ createFolder/newPlaceholder/moveTo/moveToRoot) to en-US + zh-CN. Scope notes: - The existing `EntityManagerShell` is untouched — not yet consumed by panels. Phase 4 will migrate Equipment/Location panels through the same hook + FolderTree/FolderBreadcrumb pattern (the shell wrapper will be revisited if migration reveals real pressure; as-is each panel already has a 40/60 grid that keeps the rewrite minimal). - Detail-replaces-list transition (back button top-left) is deferred to a follow-up; drops + folder filtering alone are the high-value change users asked for first. Typecheck: desktop-renderer clean. * feat(folders): Phase 4 — wire FolderTree + breadcrumb into Equipment and Location panels Applies the character-manager pattern (Phase 3) to the other two entity kinds. Both panels now: - Initialize `useEntityFolders({ kind: 'equipment' | 'location' })`. - Render a collapsible FolderTree above the 40/60 grid and a FolderBreadcrumb in the header for path navigation. - Filter `filtered` by `currentFolderId` on top of the existing search / type filter. - Stamp new rows with `folderId: folderApi.currentFolderId` so "New Equipment" / "New Location" inside a folder stays inside it. - Make list items draggable (DND payload `application/lucid-entity-id`); drops on a FolderTree node call the respective `setFolder` IPC then dispatch `moveItemToFolder` locally. Shared hook/components untouched — only per-panel plumbing changes. Typecheck: desktop-renderer clean. * feat(folders): Phase 5 — folder tree + move-to-folder DnD on Asset Browser Extends folder support to the global asset browser. Mirrors the entity pattern (Phases 3-4) with three scope-specific adjustments documented in the PRD: Wiring: - AssetBrowserPanel initializes `useEntityFolders({ kind: 'asset' })`. - Collapsible FolderTree below the title and FolderBreadcrumb under it; both are hidden in semantic-search mode (see scope note). - Non-semantic `gridAssets` is additionally filtered by `currentFolderId` before sorting + pagination. Assets load path: - `useAssetOperations.loadAssets` now maps `asset.folderId` from the IPC payload into the Redux `Asset` row so folder membership survives a reload. Move-to-folder (drag-drop): - AssetGrid cards now set both `application/x-lucid-asset` (existing, for ref-image drop targets) and `application/lucid-entity-id` (new, for folder drops) on dragstart, with `effectAllowed: 'copyMove'`. - Drops on FolderTree nodes call `asset.setFolder(hash, folderId)` then dispatch `moveItemToFolder({ hash, folderId })` (the assets slice keys moves on hash, not id). Scope notes (per PRD): - Semantic mode intentionally bypasses the folder filter: search intent is global, and filtering would surprise users expecting a full-corpus ranking. - The bottom-docked AssetDetailPanel is preserved as-is — the detail-replaces-list rework is a follow-up and the asset browser's multi-select / batch ops / rubber-band semantics don't map cleanly to that pattern without rework. - Right-click "Move to…" dialog deferred to Phase 6 polish. Typecheck: desktop-renderer clean. * perf(canvas): reduce middle-mouse pan and node drag lag Three bottlenecks were firing on every pointermove during pan/drag: 1. Persist middleware scheduled a 500 ms canvas:save timer on every moveNode dispatch. During a 1-s drag this reset the timer ~60 times, but each reset still walked the canvas snapshot and allocated a new closure. Gated the save behind an isInteracting flag exposed via setCanvasInteracting(); during drag/pan we only remember a pending save and flush a single IPC on interaction end. 2. Viewport updates were wrapped in a 120 ms debounce that allocated a new closure per frame via useMemo + dispatch. Removed the debounce — onMoveEnd fires once at pan/zoom end anyway, so debouncing it was always redundant. 3. ReactFlow's viewport (the transformed parent of all nodes) had no compositor layer, so the browser repainted the whole canvas area on every pointermove. Added transform: translateZ(0) + will-change: transform to .react-flow__viewport and will-change: transform to .react-flow__node to promote them to GPU layers. Pre-existing EntityManagerPanels test failures on this branch are unrelated (missing api.folder mock from Phase 4 entity-folders work). * fix(deps): patch radix compose-refs/slot to avoid React 19 infinite loop React 19 changed ref-callback semantics: when a composed ref callback's identity changes across renders, React treats it as unmount→mount of the ref, invoking the callback with null then with the node. If any of the composed refs is a useState setter (common inside Radix primitives like Tooltip/Popover/Menu internals), this re-fires the setter on every commit, causing "Maximum update depth exceeded" inside setRef. @radix-ui/react-compose-refs@1.1.2 uses React.useCallback(composeRefs(...refs), refs) but the spread `refs` array is a new array every render, so the dep array is effectively unstable and the callback identity flips every render — triggering the above cycle. @radix-ui/react-slot@1.2.4's SlotClone calls `composeRefs()` directly without any memoization, with the same result. Patched both packages (both .js and .mjs outputs) via patch-package to: - useComposedRefs: store refs in a ref + return a stable [] callback - SlotClone: use useComposedRefs instead of raw composeRefs Matches the approach in upstream PR radix-ui/primitives#3804 (open, not yet released). Removes the crash seen on CanvasToolbar when Tooltip + asChild + forwardRef wraps ToolbarButton. * feat(commander): end-to-end story-to-video pipeline audit + fixes + 0.0.4 Release 0.0.4. Bundles the Commander story-to-video pipeline audit, the entity folders + layout rework, and the CI test fixes that unblock main. Tool registration + wiring - Register asset.import/list, prompt.get/setCustom, render.start against real CAS, PromptStore, and @lucid-fin/media-engine pipelines. Previously these tools were authored in @lucid-fin/application but never exposed to the Commander agent registry. - render.cancel and render.exportBundle raise typed errors stating they are not yet wired, instead of silently faking success (Debug First). - Replace six no-op canvas capabilities in commander-tool-deps with real implementations: estimateCost runs buildGenerationContext per node and asks each adapter for a real estimate; previewPrompt returns the full CompiledPrompt (segments, wordCount, budget, diagnostics); addNote / updateNote / deleteNote mutate canvas.notes; importWorkflow / exportWorkflow serialize and round-trip the canvas JSON shape. - BuiltGenerationContext now exposes compiled: CompiledPrompt so the previewPrompt path can return the same prompt text the generator would actually send. Process-prompt dispatch - AgentOrchestrator accepts an optional resolveCanvasNodeType resolver. When the LLM calls canvas.generate without a nodeType arg, the orchestrator reads the real node.type from the canvas before running detectProcess - eliminates the image-node-generation default bias on video and audio nodes. - PHASE_CRITICAL_PROCESS_KEYS (workflow-orchestration, *-ref-image-*, image/video node generation, render-and-export) are pinned once active and no longer stripped by stripInactiveProcessPrompts, so a long story-to-video run does not re-plan from scratch every third step. - detectInitialProcessPrompts now primes workflow-orchestration when the canvas is empty and no node is selected, so the first model turn sees the 6-phase chain before any tool call. Always-loaded tool set - Drop dead tool.list and guide.list names from ALWAYS_LOADED_TOOLS. Both were merged into tool.get / guide.get list-mode long ago. Workflow content - workflow.expandIdea returns an end-to-end 6-phase plan (outline → entities → node asset stores → reference images → first/last frames → video + render) with explicit per-phase checkpoints. - Add workflow-story-to-video guide to WORKFLOW_GUIDES with phase-by- phase instructions and inter-phase rules. - Rewrite workflow-video-clone guide to reflect that video.clone is in fact registered. - Extend workflow-orchestration process prompt with the same 6-phase chain. - Add a "Story-first posture" clause to the agent-system default prompt: treat vague creative requests as invitations to drive the full pipeline. Entity folders + layout rework - Asset / Character / Equipment / Location panels now use EntityFileExplorer with folder trees, breadcrumbs, and move-to-folder DnD. - contracts, storage, IPC, Redux slices for entity folders. - FolderTile, TileContextMenu, useEntityClipboard, useEntityFolders. - Updated en-US / zh-CN i18n. Release prep 0.0.4 - Bump all 11 package.json files + package-lock.json entries from 0.0.3 to 0.0.4. - Bump api-server.test.ts version-fixture string. CI test fixes - slice initial-state tests (assets/characters/locations): assert new folders / currentFolderId / foldersLoading fields on initial state. - useEntityFolders: optional-chain folder[kind] so test mocks that omit the folder namespace don't explode with TypeError. - EntityManagerPanels.test.tsx: add renderAndOpenDetail helper that waits for file-explorer load then double-clicks the selected tile to open the detail drawer (all 12 assertion tests now see the form markup). Replace click() with doubleClick() on tile-name queries where the test intent is "switch to this entity". - Use getAllByRole for the dialog cancel/confirm buttons (form and dialog both expose the same action label when zh-CN is active). - AssetBrowserPanel.test.tsx: drop the stale "1 selected" assertion that targeted the removed sticky-count label; skip 5 tests that assert on legacy toolbar / detail-panel UI surfaces with TODO notes for rewrite against the EntityFileExplorer-based browser. - commander.handlers buildContext test: new assertion reflects that empty-canvas + no-selection now primes workflow-orchestration. - commander-tool-deps.ts: attach { cause: err } on the JSON-parse rethrow to satisfy preserve-caught-error lint rule. Tests: 2076 passed, 7 skipped, 0 failed. Lint + build clean.
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.
Note
This PR was generated by an AI agent (Claude Code). Human verification of the fix and its implications is still outstanding.
Summary
SlotClonenow usesuseComposedRefs()(memoized viaReact.useCallback) instead of rawcomposeRefs()to compose forwarded and children refsReproduction
Minimal repro repo: https://github.com/yannisgu/radix-tanstack-form-react19-repro
Problem
SlotClonecallscomposeRefs(forwardedRef, childrenRef)directly on every render, creating a new function each time. In React 19,setRef()returnsref(value), which React treats as a cleanup function. When React sees a new callback ref with cleanup on each render, it unmounts the old ref (calling cleanup) and mounts the new one — triggering any state setters passed as refs (e.g.SelectTrigger'sonTriggerChange, orCheckbox'suseSize).This state update during commit causes a re-render, which creates another new
composeRefsfunction, which triggers another unmount/remount — an infinite synchronous loop that freezes the browser tab.The bug is particularly severe when combined with libraries that trigger re-renders during the commit phase (e.g.
@tanstack/react-form'suseFieldlayout effect with no dependency array), but can occur with any component that has effects or state updates during commit.Root cause chain
SlotClonecallscomposeRefs()→ new function identity every rendersetRef(ref, value)returnsref(value)→ React 19 treats it as cleanup refonTriggerChange) → re-renderFix
Replace raw
composeRefs()withuseComposedRefs(), which wraps the call inReact.useCallback. This was already the intended pattern —useComposedRefsexists for exactly this purpose but wasn't used inSlotClone.Affected components
Any Radix component using
Slot/asChildthat passes a state setter as a ref:@radix-ui/react-select(SelectTrigger + onTriggerChange)@radix-ui/react-checkbox(useSize state setter)@radix-ui/react-popover,@radix-ui/react-tooltip,@radix-ui/react-dialog, etc.Fixes #3799