Repository navigation
Conversation
|
We hit the same bug independently and ship this exact change as a patch-package patch on 0.86.3, so +1 from production. Two things that may help this land: A second trigger, on Android. Our list is a chat transcript ( Since #59134 (2c635d7), the regression test in this PR passes on it('handles a maintainVisibleContentPosition prepend batched with a cells update that moves the window', async () => {
const items = generateItems(20);
const ITEM_HEIGHT = 10;
const list = data => (
<VirtualizedList
initialNumToRender={1}
windowSize={1}
maintainVisibleContentPosition={{minIndexForVisible: 0}}
{...baseItemProps(data)}
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
/>
);
let component;
await act(() => {
component = create(list(items));
});
await act(() => {
simulateLayout(component, {
viewport: {width: 10, height: 50},
content: {width: 10, height: items.length * ITEM_HEIGHT},
});
performAllBatches();
});
const instance = component.getInstance();
expect(instance.state.cellsAroundViewport).toEqual({first: 0, last: 4});
// Scroll one viewport down and prepend 10 items from the same event. The
// cells update has to move the window, so it isn't skipped, and its updater
// runs with the new props.
await act(() => {
simulateScroll(component, {x: 0, y: 50});
component.update(list([...generateItems(10, items.length), ...items]));
});
// Rows 5..9, now in the viewport, are at 15..19 after the prepend.
// main: {first: 4, last: 9}, pendingScrollUpdateCount 0, firstVisibleItemKey 0
expect(instance.state.cellsAroundViewport).toEqual({first: 14, last: 19});
expect(instance.state.pendingScrollUpdateCount).toBe(1);
expect(instance.state.firstVisibleItemKey).toBe(20);
});
it('handles a maintainVisibleContentPosition prepend after an equal-count change at the head', async () => {
const items = generateItems(20);
const ITEM_HEIGHT = 10;
const list = data => (
<VirtualizedList
initialNumToRender={1}
windowSize={1}
maintainVisibleContentPosition={{minIndexForVisible: 0}}
{...baseItemProps(data)}
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
/>
);
let component;
await act(() => {
component = create(list(items));
});
await act(() => {
simulateLayout(component, {
viewport: {width: 10, height: 50},
content: {width: 10, height: items.length * ITEM_HEIGHT},
});
performAllBatches();
});
const instance = component.getInstance();
// The head item leaves and one is appended: the count holds, the key at
// minIndexForVisible changes.
const shiftedItems = [...generateItems(19, 1), {key: 20}];
await act(() => {
component.update(list(shiftedItems));
performAllBatches();
});
await act(() => {
component.update(list([...generateItems(10, 100), ...shiftedItems]));
});
// main: {first: 0, last: 4}; firstVisibleItemKey was left at 0 by the update above
expect(instance.state.cellsAroundViewport).toEqual({first: 10, last: 14});
expect(instance.state.firstVisibleItemKey).toBe(100);
});How I ran them: |
Summary:
VirtualizedList.getDerivedStateFromPropsreturns early when the item count equalsprevState.renderMask.numCells(). React runs queued_updateCellsToRenderupdaters before derived state, so when a cells update renders together with a data change, the mask already has the new count and amaintainVisibleContentPositionprepend is skipped. This reads the key atminIndexForVisiblebefore the early return, and on mVCP lists only returns early when both the count and the key are unchanged.Fixes #58909. Repro: https://github.com/mozzius/virtualizedlist-batched-prepend-repro
Changelog:
[GENERAL] [FIXED] - VirtualizedList: handle a maintainVisibleContentPosition prepend that renders together with a cells update
Test Plan:
VirtualizedList-test.jsfails before the change and passes after. Existing virtualized-lists tests and snapshots are unchanged.yarn flow-checkandyarn lintare clean.🤖 Generated with Claude Code