fix(useId): prevent FinalizationRegistry entry leak in useId#9853
Open
reidbarber wants to merge 5 commits into
Open
fix(useId): prevent FinalizationRegistry entry leak in useId#9853reidbarber wants to merge 5 commits into
reidbarber wants to merge 5 commits into
Conversation
|
Build successful! 🎉 |
snowystinger
previously approved these changes
Mar 28, 2026
devongovett
reviewed
Mar 30, 2026
| } | ||
| registry.register(cleanupRef, res, cleanupRef); | ||
| registeredIds.set(cleanupRef, res); | ||
| } |
Member
There was a problem hiding this comment.
Does it work to do this in an effect instead of during render? Then it would be easier to do cleanup
Member
Author
There was a problem hiding this comment.
Don't we need the map in mergeIds during render?
Member
There was a problem hiding this comment.
we need the map, but not necessarily the finalization registry. or are you saying in a case where a component rendered, registered an id in the map, but never actually mounted?
Contributor
There was a problem hiding this comment.
Wouldn't that scenario be rather common in React 19+, since a hidden Activity subtree may never reveal?
|
Build successful! 🎉 |
## API Changes
react-aria-components/react-aria-components:TreeRenderProps TreeRenderProps {
allowsDragging: boolean
- isDropTarget: boolean
isEmpty: boolean
isFocusVisible: boolean
isFocused: boolean
selectionMode: SelectionMode
}/react-aria-components:TreeEmptyStateRenderProps TreeEmptyStateRenderProps {
allowsDragging: boolean
- isDropTarget: boolean
isFocusVisible: boolean
isFocused: boolean
selectionMode: SelectionMode
state: TreeState<unknown>@react-aria/grid/@react-aria/grid:GridKeyboardDelegate GridKeyboardDelegate <C extends IGridCollection<T>, T> {
collection: IGridCollection<T>
constructor: (GridKeyboardDelegateOptions<IGridCollection<T>>) => void
getFirstKey: (Key, boolean) => Key | null
- getKeyAbove: (Key, {
- includeDisabled?: boolean
-}) => Key | null
- getKeyBelow: (Key, {
- includeDisabled?: boolean
-}) => Key | null
+ getKeyAbove: (Key) => Key | null
+ getKeyBelow: (Key) => Key | null
getKeyForSearch: (string, Key) => Key | null
getKeyLeftOf: (Key) => Key | null
getKeyPageAbove: (Key) => Key | null
getKeyPageBelow: (Key) => Key | null
getLastKey: (Key, boolean) => Key | null
}@react-aria/selection/@react-aria/selection:ListKeyboardDelegate ListKeyboardDelegate <T> {
constructor: (Array<any>) => void
getFirstKey: () => Key | null
- getKeyAbove: (Key, {
- includeDisabled?: boolean
-}) => Key | null
- getKeyBelow: (Key, {
- includeDisabled?: boolean
-}) => Key | null
+ getKeyAbove: (Key) => Key | null
+ getKeyBelow: (Key) => Key | null
getKeyForSearch: (string, Key) => Key | null
- getKeyLeftOf: (Key, {
- includeDisabled?: boolean
-}) => Key | null
+ getKeyLeftOf: (Key) => Key | null
getKeyPageAbove: (Key) => Key | null
getKeyPageBelow: (Key) => Key | null
- getKeyRightOf: (Key, {
- includeDisabled?: boolean
-}) => Key | null
+ getKeyRightOf: (Key) => Key | null
getLastKey: () => Key | null
- getNextKey: (Key, {
- includeDisabled?: boolean
-}) => Key | null
- getPreviousKey: (Key, {
- includeDisabled?: boolean
-}) => Key | null
+ getNextKey: (Key) => Key | null
+ getPreviousKey: (Key) => Key | null
}@react-spectrum/s2/@react-spectrum/s2:ListView ListView <T extends {}> {
UNSAFE_className?: UnsafeClassName
UNSAFE_style?: CSSProperties
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
autoFocus?: boolean | FocusStrategy
children: ReactNode | ({}) => ReactNode
defaultSelectedKeys?: 'all' | Iterable<Key>
dependencies?: ReadonlyArray<any>
disabledBehavior?: DisabledBehavior = 'all'
disabledKeys?: Iterable<Key>
disallowEmptySelection?: boolean
disallowTypeAhead?: boolean = false
- dragAndDropHooks?: DragAndDropHooks<NoInfer<T>>
escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
hideLinkOutIcon?: boolean
id?: string
isQuiet?: boolean
loadingState?: LoadingState
onAction?: (Key) => void
onLoadMore?: () => void
onSelectionChange?: (Selection) => void
overflowMode?: 'wrap' | 'truncate' = 'truncate'
renderActionBar?: ('all' | Set<Key>) => ReactElement
renderEmptyState?: (GridListRenderProps) => ReactNode
selectedKeys?: 'all' | Iterable<Key>
selectionMode?: SelectionMode
selectionStyle?: 'highlight' | 'checkbox' = 'checkbox'
shouldSelectOnPressUp?: boolean
slot?: string | null
styles?: StylesPropWithHeight
}/@react-spectrum/s2:DragPreview-DragPreview {
- children?: ReactNode
- items: Array<DragItem>
- overflowMode?: 'wrap' | 'truncate'
-}/@react-spectrum/s2:TableView TableView {
UNSAFE_className?: UnsafeClassName
UNSAFE_style?: CSSProperties
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
children?: ReactNode
defaultExpandedKeys?: Iterable<Key>
defaultSelectedKeys?: 'all' | Iterable<Key>
density?: 'compact' | 'spacious' | 'regular' = 'regular'
disabledBehavior?: DisabledBehavior = 'all'
disabledKeys?: Iterable<Key>
disallowEmptySelection?: boolean
- dragAndDropHooks?: DragAndDropHooks
escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
expandedKeys?: Iterable<Key>
id?: string
isQuiet?: boolean
onAction?: (Key) => void
onExpandedChange?: (Set<Key>) => any
onLoadMore?: () => any
onResize?: (Map<Key, ColumnSize>) => void
onResizeEnd?: (Map<Key, ColumnSize>) => void
onResizeStart?: (Map<Key, ColumnSize>) => void
onSelectionChange?: (Selection) => void
onSortChange?: (SortDescriptor) => any
overflowMode?: 'wrap' | 'truncate' = 'truncate'
renderActionBar?: ('all' | Set<Key>) => ReactElement
selectedKeys?: 'all' | Iterable<Key>
selectionMode?: SelectionMode
shouldSelectOnPressUp?: boolean
slot?: string | null
sortDescriptor?: SortDescriptor
styles?: StylesPropWithHeight
treeColumn?: Key
}/@react-spectrum/s2:TreeView TreeView <T extends {}> {
UNSAFE_className?: UnsafeClassName
UNSAFE_style?: CSSProperties
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
autoFocus?: boolean | FocusStrategy
children?: ReactNode | (T) => ReactNode
defaultExpandedKeys?: Iterable<Key>
defaultSelectedKeys?: 'all' | Iterable<Key>
dependencies?: ReadonlyArray<any>
disabledBehavior?: DisabledBehavior = 'all'
disabledKeys?: Iterable<Key>
disallowEmptySelection?: boolean
- dragAndDropHooks?: DragAndDropHooks<NoInfer<T>>
escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
expandedKeys?: Iterable<Key>
id?: string
items?: Iterable<T>
onExpandedChange?: (Set<Key>) => any
onSelectionChange?: (Selection) => void
renderActionBar?: ('all' | Set<Key>) => ReactElement
renderEmptyState?: (TreeEmptyStateRenderProps) => ReactNode
selectedKeys?: 'all' | Iterable<Key>
selectionMode?: SelectionMode
selectionStyle?: 'highlight' | 'checkbox' = 'checkbox'
shouldSelectOnPressUp?: boolean
slot?: string | null
styles?: StylesPropWithHeight
}/@react-spectrum/s2:DIRECTORY_DRAG_TYPE-DIRECTORY_DRAG_TYPE {
- UNTYPED
-}/@react-spectrum/s2:isDirectoryDropItem-isDirectoryDropItem {
- dropItem: DropItem
- returnVal: undefined
-}/@react-spectrum/s2:isFileDropItem-isFileDropItem {
- dropItem: DropItem
- returnVal: undefined
-}/@react-spectrum/s2:isTextDropItem-isTextDropItem {
- dropItem: DropItem
- returnVal: undefined
-}/@react-spectrum/s2:useDragAndDrop-useDragAndDrop <T = {}> {
- options: DragAndDropOptions<T>
- returnVal: undefined
-}/@react-spectrum/s2:ListViewProps ListViewProps <T> {
UNSAFE_className?: UnsafeClassName
UNSAFE_style?: CSSProperties
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
autoFocus?: boolean | FocusStrategy
children: ReactNode | (T) => ReactNode
defaultSelectedKeys?: 'all' | Iterable<Key>
dependencies?: ReadonlyArray<any>
disabledBehavior?: DisabledBehavior = 'all'
disabledKeys?: Iterable<Key>
disallowEmptySelection?: boolean
disallowTypeAhead?: boolean = false
- dragAndDropHooks?: DragAndDropHooks<NoInfer<T>>
escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
hideLinkOutIcon?: boolean
id?: string
isQuiet?: boolean
loadingState?: LoadingState
onAction?: (Key) => void
onLoadMore?: () => void
onSelectionChange?: (Selection) => void
overflowMode?: 'wrap' | 'truncate' = 'truncate'
renderActionBar?: ('all' | Set<Key>) => ReactElement
renderEmptyState?: (GridListRenderProps) => ReactNode
selectedKeys?: 'all' | Iterable<Key>
selectionMode?: SelectionMode
selectionStyle?: 'highlight' | 'checkbox' = 'checkbox'
shouldSelectOnPressUp?: boolean
slot?: string | null
styles?: StylesPropWithHeight
}/@react-spectrum/s2:DragPreviewProps-DragPreviewProps {
- children?: ReactNode
- items: Array<DragItem>
- overflowMode?: 'wrap' | 'truncate'
-}/@react-spectrum/s2:TableViewProps TableViewProps {
UNSAFE_className?: UnsafeClassName
UNSAFE_style?: CSSProperties
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
children?: ReactNode
defaultExpandedKeys?: Iterable<Key>
defaultSelectedKeys?: 'all' | Iterable<Key>
density?: 'compact' | 'spacious' | 'regular' = 'regular'
disabledBehavior?: DisabledBehavior = 'all'
disabledKeys?: Iterable<Key>
disallowEmptySelection?: boolean
- dragAndDropHooks?: DragAndDropHooks
escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
expandedKeys?: Iterable<Key>
id?: string
isQuiet?: boolean
onAction?: (Key) => void
onExpandedChange?: (Set<Key>) => any
onLoadMore?: () => any
onResize?: (Map<Key, ColumnSize>) => void
onResizeEnd?: (Map<Key, ColumnSize>) => void
onResizeStart?: (Map<Key, ColumnSize>) => void
onSelectionChange?: (Selection) => void
onSortChange?: (SortDescriptor) => any
overflowMode?: 'wrap' | 'truncate' = 'truncate'
renderActionBar?: ('all' | Set<Key>) => ReactElement
selectedKeys?: 'all' | Iterable<Key>
selectionMode?: SelectionMode
shouldSelectOnPressUp?: boolean
slot?: string | null
sortDescriptor?: SortDescriptor
styles?: StylesPropWithHeight
treeColumn?: Key
}/@react-spectrum/s2:TreeViewProps TreeViewProps <T> {
UNSAFE_className?: UnsafeClassName
UNSAFE_style?: CSSProperties
aria-describedby?: string
aria-details?: string
aria-label?: string
aria-labelledby?: string
autoFocus?: boolean | FocusStrategy
children?: ReactNode | (T) => ReactNode
defaultExpandedKeys?: Iterable<Key>
defaultSelectedKeys?: 'all' | Iterable<Key>
dependencies?: ReadonlyArray<any>
disabledBehavior?: DisabledBehavior = 'all'
disabledKeys?: Iterable<Key>
disallowEmptySelection?: boolean
- dragAndDropHooks?: DragAndDropHooks<NoInfer<T>>
escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
expandedKeys?: Iterable<Key>
id?: string
items?: Iterable<T>
onExpandedChange?: (Set<Key>) => any
onSelectionChange?: (Selection) => void
renderActionBar?: ('all' | Set<Key>) => ReactElement
renderEmptyState?: (TreeEmptyStateRenderProps) => ReactNode
selectedKeys?: 'all' | Iterable<Key>
selectionMode?: SelectionMode
selectionStyle?: 'highlight' | 'checkbox' = 'checkbox'
shouldSelectOnPressUp?: boolean
slot?: string | null
styles?: StylesPropWithHeight
}/@react-spectrum/s2:DragAndDropHooks-DragAndDropHooks {
- D: undefined
-}/@react-spectrum/s2:DragAndDropOptions-DragAndDropOptions <T = {}> {
- acceptedDragTypes?: 'all' | Array<string | symbol> = 'all'
- dropTargetDelegate?: DropTargetDelegate
- getAllowedDropOperations?: () => Array<DropOperation>
- getDropOperation?: (DropTarget, DragTypes, Array<DropOperation>) => DropOperation
- getItems?: (Set<Key>, Array<T>) => Array<DragItem> = () => []
- isDisabled?: boolean
- onDragEnd?: (DraggableCollectionEndEvent) => void
- onDragMove?: (DraggableCollectionMoveEvent) => void
- onDragStart?: (DraggableCollectionStartEvent) => void
- onDrop?: (DroppableCollectionDropEvent) => void
- onDropActivate?: (DroppableCollectionActivateEvent) => void
- onDropEnter?: (DroppableCollectionEnterEvent) => void
- onDropExit?: (DroppableCollectionExitEvent) => void
- onInsert?: (DroppableCollectionInsertDropEvent) => void
- onItemDrop?: (DroppableCollectionOnItemDropEvent) => void
- onMove?: (DroppableCollectionReorderEvent) => void
- onReorder?: (DroppableCollectionReorderEvent) => void
- onRootDrop?: (DroppableCollectionRootDropEvent) => void
- renderDragPreview?: (Array<DragItem>) => JSX.Element | {
- element: JSX.Element
- x: number
- y: number
-}
- renderDropIndicator?: (DropTarget) => JSX.Element
- shouldAcceptItemDrop?: (ItemDropTarget, DragTypes) => boolean
-}/@react-spectrum/s2:DirectoryDropItem-DirectoryDropItem {
- getEntries: () => AsyncIterable<FileDropItem | DirectoryDropItem>
- kind: 'directory'
- name: string
-}/@react-spectrum/s2:DraggableCollectionEndEvent-DraggableCollectionEndEvent {
- dropOperation: DropOperation
- isInternal: boolean
- keys: Set<Key>
- type: 'dragend'
- x: number
- y: number
-}/@react-spectrum/s2:DraggableCollectionMoveEvent-DraggableCollectionMoveEvent {
- keys: Set<Key>
- type: 'dragmove'
- x: number
- y: number
-}/@react-spectrum/s2:DraggableCollectionStartEvent-DraggableCollectionStartEvent {
- keys: Set<Key>
- type: 'dragstart'
- x: number
- y: number
-}/@react-spectrum/s2:DragPreviewRenderer-DragPreviewRenderer {
- D: undefined
-}/@react-spectrum/s2:DragTypes-DragTypes {
- has: (string | symbol) => boolean
-}/@react-spectrum/s2:DropItem-DropItem {
- D: undefined
-}/@react-spectrum/s2:DropOperation-DropOperation {
- D: undefined
-}/@react-spectrum/s2:DroppableCollectionDropEvent-DroppableCollectionDropEvent {
- dropOperation: DropOperation
- items: Array<DropItem>
- target: DropTarget
- type: 'drop'
- x: number
- y: number
-}/@react-spectrum/s2:DroppableCollectionEnterEvent-DroppableCollectionEnterEvent {
- target: DropTarget
- type: 'dropenter'
- x: number
- y: number
-}/@react-spectrum/s2:DroppableCollectionExitEvent-DroppableCollectionExitEvent {
- target: DropTarget
- type: 'dropexit'
- x: number
- y: number
-}/@react-spectrum/s2:DroppableCollectionInsertDropEvent-DroppableCollectionInsertDropEvent {
- dropOperation: DropOperation
- items: Array<DropItem>
- target: ItemDropTarget
-}/@react-spectrum/s2:DroppableCollectionMoveEvent-DroppableCollectionMoveEvent {
- target: DropTarget
- type: 'dropmove'
- x: number
- y: number
-}/@react-spectrum/s2:DroppableCollectionOnItemDropEvent-DroppableCollectionOnItemDropEvent {
- dropOperation: DropOperation
- isInternal: boolean
- items: Array<DropItem>
- target: ItemDropTarget
-}/@react-spectrum/s2:DroppableCollectionReorderEvent-DroppableCollectionReorderEvent {
- dropOperation: DropOperation
- keys: Set<Key>
- target: ItemDropTarget
-}/@react-spectrum/s2:DroppableCollectionRootDropEvent-DroppableCollectionRootDropEvent {
- dropOperation: DropOperation
- items: Array<DropItem>
-}/@react-spectrum/s2:DropPosition-DropPosition {
- D: undefined
-}/@react-spectrum/s2:DropTarget-DropTarget {
- D: undefined
-}/@react-spectrum/s2:FileDropItem-FileDropItem {
- getFile: () => Promise<File>
- getText: () => Promise<string>
- kind: 'file'
- name: string
- type: string
-}/@react-spectrum/s2:ItemDropTarget-ItemDropTarget {
- dropPosition: DropPosition
- key: Key
- type: 'item'
-}/@react-spectrum/s2:RootDropTarget-RootDropTarget {
- type: 'root'
-}/@react-spectrum/s2:TextDropItem-TextDropItem {
- getText: (string) => Promise<string>
- kind: 'text'
- types: Set<string>
-} |
snowystinger
approved these changes
May 18, 2026
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.
Closes #9852
Fixes
useIdsoFinalizationRegistryentries do not accumulate on every re-render. Previously, they would until the component was unmounted.✅ Pull Request Checklist:
📝 Test Instructions:
Unit test should cover.
🧢 Your Project: