diff --git a/packages/virtualized-lists/Lists/VirtualizedList.js b/packages/virtualized-lists/Lists/VirtualizedList.js index a8593403e4c..8545ba4c6ff 100644 --- a/packages/virtualized-lists/Lists/VirtualizedList.js +++ b/packages/virtualized-lists/Lists/VirtualizedList.js @@ -777,7 +777,7 @@ class VirtualizedList extends StateSafePureComponent< firstVisibleItemKey: newFirstVisibleItemKey, pendingScrollUpdateCount: maintainVisibleContentPositionAdjustment != null - ? prevState.pendingScrollUpdateCount + 1 + ? 1 : prevState.pendingScrollUpdateCount, }; } @@ -1760,9 +1760,7 @@ class VirtualizedList extends StateSafePureComponent< zoomScale, }; if (this.state.pendingScrollUpdateCount > 0) { - this.setState<'pendingScrollUpdateCount'>(state => ({ - pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1, - })); + this.setState<'pendingScrollUpdateCount'>({pendingScrollUpdateCount: 0}); } this._updateViewableItems(this.props, this.state.cellsAroundViewport); if (!this.props) { diff --git a/packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js b/packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js index 6547145e920..661be1b1f83 100644 --- a/packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js +++ b/packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js @@ -2877,6 +2877,83 @@ it('maintainVisibleContentPosition with inverted VirtualizedList handles prepend expect(anchorAfterPrepend).toBeLessThanOrEqual(anchorBeforePrepend + 10); }); +it('handles rapid prepends with coalesced scroll event (regression for #53542)', async () => { + const items = generateItems(20); + const ITEM_HEIGHT = 10; + + let component; + await act(() => { + component = create( + , + ); + }); + + await act(() => { + simulateLayout(component, { + viewport: {width: 10, height: 50}, + content: {width: 10, height: items.length * ITEM_HEIGHT}, + }); + simulateScroll(component, {x: 0, y: 50}); + performAllBatches(); + }); + + const afterFirstPrepend = [...generateItems(5, items.length), ...items]; + const afterSecondPrepend = [ + ...generateItems(3, afterFirstPrepend.length), + ...afterFirstPrepend, + ]; + + // Two rapid prepends WITHOUT intermediate scroll (coalesced native event) + await act(() => { + component.update( + , + ); + }); + + await act(() => { + component.update( + , + ); + }); + + // Only ONE coalesced scroll event for both prepends (delta 8*ITEM_HEIGHT) + // This simulates EventQueue coalescing: dispatchUniqueEvent replaces previous scroll + // Previously: pending 0→1→2, scroll 2→1 (still blocked). Now: 0→1→1→0 (unblocked) + await act(() => { + simulateContentLayout(component, { + width: 10, + height: afterSecondPrepend.length * ITEM_HEIGHT, + }); + simulateScroll(component, {x: 0, y: 50 + 8 * ITEM_HEIGHT}); + performAllBatches(); + }); + + // Pending should be 0, not 1 – this fails on main before fix + expect(component.getInstance().state.pendingScrollUpdateCount).toBe(0); + expect(component.getInstance().state.firstVisibleItemKey).not.toBeNull(); + expect( + component.getInstance().state.cellsAroundViewport.first, + ).toBeGreaterThanOrEqual(0); +}); + function generateItems(count, startKey = 0) { return Array(count) .fill()