From 86abc7b383d5e519bd604473e560d0e3a3cd8260 Mon Sep 17 00:00:00 2001 From: Dheeraj Koppu Date: Fri, 6 Nov 2026 17:21:35 -0800 Subject: [PATCH 1/2] Avoid reattaching VirtualizedList cell refs on every render VirtualizedList gave every cell a new ref callback on each render, so React detached and reattached every mounted cell's ref even when the cells bailed out of rendering. Use one stable callback that reads the key from the cell's cellKey prop, and rely on _onCellUnmount to remove entries for unmounted cells. --- .../Lists/VirtualizedList.js | 12 +++-- .../Lists/__tests__/VirtualizedList-test.js | 53 +++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/packages/virtualized-lists/Lists/VirtualizedList.js b/packages/virtualized-lists/Lists/VirtualizedList.js index 031267465af8..2224211f8fbf 100644 --- a/packages/virtualized-lists/Lists/VirtualizedList.js +++ b/packages/virtualized-lists/Lists/VirtualizedList.js @@ -1017,9 +1017,7 @@ class VirtualizedList extends StateSafePureComponent< onUpdateSeparators={this._onUpdateSeparators} onCellFocusCapture={this._onCellFocusCapture} onUnmount={this._onCellUnmount} - ref={ref => { - this._cellRefs[key] = ref; - }} + ref={this._captureCellRef} renderItem={renderItem} {...(shouldListenForLayout && { onCellLayout: this._onCellLayout, @@ -1610,6 +1608,14 @@ class VirtualizedList extends StateSafePureComponent< } }; + // Stable so React does not detach and reattach every cell's ref on each render. + // Unmounted cells are removed in `_onCellUnmount`. + _captureCellRef = (cellRenderer: null | CellRenderer): void => { + if (cellRenderer != null) { + this._cellRefs[cellRenderer.props.cellKey] = cellRenderer; + } + }; + _onCellUnmount = (cellKey: string) => { delete this._cellRefs[cellKey]; this._listMetrics.notifyCellUnmounted(cellKey); diff --git a/packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js b/packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js index 5bc0254c7127..bb0ea124c632 100644 --- a/packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js +++ b/packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js @@ -294,6 +294,59 @@ describe('VirtualizedList', () => { }); }); + it('does not reattach cell refs when the list re-renders', async () => { + const listRef = createRef(); + const items = generateItems(5); + let component; + await act(() => { + component = create( + , + ); + }); + + const cellRefs = listRef.current._cellRefs; + const cellRefWrites = []; + listRef.current._cellRefs = new Proxy(cellRefs, { + set(target, cellKey, cellRenderer) { + cellRefWrites.push(cellKey); + target[cellKey] = cellRenderer; + return true; + }, + }); + + await act(() => { + component.update( + , + ); + }); + + expect(cellRefWrites).toEqual([]); + expect(Object.keys(cellRefs)).toEqual(['0', '1', '2', '3', '4']); + }); + + it('removes cell refs when cells unmount', async () => { + const listRef = createRef(); + let component; + await act(() => { + component = create( + , + ); + }); + + await act(() => { + component.update( + , + ); + }); + + expect(Object.keys(listRef.current._cellRefs)).toEqual(['0', '1']); + expect(listRef.current._cellRefs[0].props.cellKey).toBe(0); + }); + it('handles nested lists', async () => { let component; await act(() => { From 3a8790102219aa5b5b95b18a792ed47652e3a225 Mon Sep 17 00:00:00 2001 From: Dheeraj Koppu Date: Fri, 6 Nov 2026 17:28:03 -0800 Subject: [PATCH 2/2] Wrap the _captureCellRef comment at 80 columns --- packages/virtualized-lists/Lists/VirtualizedList.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/virtualized-lists/Lists/VirtualizedList.js b/packages/virtualized-lists/Lists/VirtualizedList.js index 2224211f8fbf..18b536b12e38 100644 --- a/packages/virtualized-lists/Lists/VirtualizedList.js +++ b/packages/virtualized-lists/Lists/VirtualizedList.js @@ -1608,8 +1608,8 @@ class VirtualizedList extends StateSafePureComponent< } }; - // Stable so React does not detach and reattach every cell's ref on each render. - // Unmounted cells are removed in `_onCellUnmount`. + // Stable, so React doesn't detach and reattach every cell's ref on each + // render. Unmounted cells are removed in `_onCellUnmount`. _captureCellRef = (cellRenderer: null | CellRenderer): void => { if (cellRenderer != null) { this._cellRefs[cellRenderer.props.cellKey] = cellRenderer;