Skip to content

Commit 6dcc04b

Browse files
javachemeta-codesync[bot]
authored andcommitted
Release high-priority list updates after no-op commits (#59295)
Summary: Pull Request resolved: #59295 `VirtualizedList` can leave its high-priority update flag set when a queued cells updater becomes a no-op, because `componentDidUpdate` does not run. Complete the high-priority attempt using a `setState` callback so the next urgent window update does not wait for the batching timer. Scope completion to its attempt so an older callback cannot clear newer work scheduled during `componentDidUpdate` or `onEndReached`. Preserve the no-op fast path and the existing once-per-commit scheduling protection. Changelog: [General][Fixed] - Avoid delaying urgent `VirtualizedList` rendering after queued no-op updates Reviewed By: andrewdacenko Differential Revision: D126635244 fbshipit-source-id: 174d822412f048d556642eb358103ea9a811320f
1 parent 871ab0d commit 6dcc04b

2 files changed

Lines changed: 113 additions & 6 deletions

File tree

‎packages/virtualized-lists/Lists/VirtualizedList.js‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1482,6 +1482,7 @@ class VirtualizedList extends StateSafePureComponent<
14821482
_hasWarned: {[string]: boolean} = {};
14831483
_headerLength = 0;
14841484
_hiPriInProgress: boolean = false; // flag to prevent infinite hiPri cell limit update
1485+
_hiPriUpdateId: number = 0;
14851486
_indicesToKeys: Map<number, string> = new Map();
14861487
_isAncestorSuppressed: boolean = false;
14871488
_lastFocusedCellKey: ?string = null;
@@ -2094,7 +2095,12 @@ class VirtualizedList extends StateSafePureComponent<
20942095
clearTimeout(this._updateCellsToRenderTimeoutID);
20952096
this._updateCellsToRenderTimeoutID = null;
20962097
}
2097-
this._updateCellsToRender();
2098+
const hiPriUpdateId = ++this._hiPriUpdateId;
2099+
this._updateCellsToRender(() => {
2100+
if (this._hiPriUpdateId === hiPriUpdateId) {
2101+
this._hiPriInProgress = false;
2102+
}
2103+
});
20982104
return;
20992105
} else {
21002106
if (this._updateCellsToRenderTimeoutID == null) {
@@ -2200,7 +2206,7 @@ class VirtualizedList extends StateSafePureComponent<
22002206
this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e);
22012207
};
22022208

2203-
_updateCellsToRender = () => {
2209+
_updateCellsToRender = (onUpdateComplete?: () => void) => {
22042210
this._updateViewableItems(this.props, this.state.cellsAroundViewport);
22052211
this._notifyCrossOrientationChildren();
22062212

@@ -2217,17 +2223,15 @@ class VirtualizedList extends StateSafePureComponent<
22172223
this._listMetrics.getContentLength() > 0 &&
22182224
VirtualizedList._computeCellsUpdate(this, this.props, this.state) == null
22192225
) {
2220-
// No update means no componentDidUpdate to reset this, which would
2221-
// push the next hiPri update onto the batching timer.
2222-
this._hiPriInProgress = false;
2226+
onUpdateComplete?.();
22232227
return;
22242228
}
22252229

22262230
this._hasPendingStateUpdate = true;
22272231
this.setState<'cellsAroundViewport' | 'renderMask'>((state, props) => {
22282232
this._hasPendingStateUpdate = false;
22292233
return VirtualizedList._computeCellsUpdate(this, props, state);
2230-
});
2234+
}, onUpdateComplete);
22312235
};
22322236

22332237
_createViewToken = (

‎packages/virtualized-lists/Lists/__tests__/VirtualizedList-test.js‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1695,6 +1695,17 @@ it('does not move render area when initialScrollIndex is > 0 and offset not yet
16951695
// 5 cells should be present starting at index 1, since we have not seen a
16961696
// scroll event yet for current position.
16971697
expect(component).toMatchSnapshot();
1698+
const instance = component.getInstance();
1699+
expect(instance.state.pendingScrollUpdateCount).toBe(1);
1700+
expect(instance._hiPriInProgress).toBe(false);
1701+
1702+
await act(() => {
1703+
simulateScroll(component, {x: 0, y: 10});
1704+
});
1705+
1706+
expect(instance.state.pendingScrollUpdateCount).toBe(0);
1707+
expect(instance.state.cellsAroundViewport.last).toBeGreaterThan(5);
1708+
expect(instance._hiPriInProgress).toBe(false);
16981709
});
16991710

17001711
it('clamps render area when items removed for initialScrollIndex > 0 and scroller position not yet known', async () => {
@@ -2278,6 +2289,7 @@ it('renders windowSize derived region at top', async () => {
22782289
it('does not update state on a scroll that keeps the render window', async () => {
22792290
const items = generateItems(10);
22802291
const ITEM_HEIGHT = 10;
2292+
const onEndReached = jest.fn();
22812293

22822294
let component;
22832295
await act(() => {
@@ -2286,6 +2298,7 @@ it('does not update state on a scroll that keeps the render window', async () =>
22862298
initialNumToRender={1}
22872299
maxToRenderPerBatch={1}
22882300
windowSize={3}
2301+
onEndReached={onEndReached}
22892302
{...baseItemProps(items)}
22902303
{...fixedHeightItemLayoutProps(ITEM_HEIGHT)}
22912304
/>,
@@ -2314,6 +2327,96 @@ it('does not update state on a scroll that keeps the render window', async () =>
23142327
});
23152328

23162329
expect(setStateSpy).not.toHaveBeenCalled();
2330+
2331+
const instance = component.getInstance();
2332+
const componentDidUpdateSpy = jest.spyOn(instance, 'componentDidUpdate');
2333+
2334+
await act(() => {
2335+
// The high-priority update reads the final scroll position when React
2336+
// processes it, so returning to the top makes the queued update a no-op.
2337+
simulateScroll(component, {x: 0, y: 60});
2338+
simulateScroll(component, {x: 0, y: 0});
2339+
});
2340+
2341+
expect(setStateSpy).toHaveBeenCalledTimes(1);
2342+
expect(componentDidUpdateSpy).not.toHaveBeenCalled();
2343+
expect(instance._hiPriInProgress).toBe(false);
2344+
2345+
const previousWindow = instance.state.cellsAroundViewport;
2346+
await act(() => {
2347+
simulateScroll(component, {x: 0, y: 60});
2348+
});
2349+
2350+
// No timer tick should be needed for the next high-priority update.
2351+
expect(instance.state.cellsAroundViewport.last).toBeGreaterThan(
2352+
previousWindow.last,
2353+
);
2354+
expect(componentDidUpdateSpy).toHaveBeenCalledTimes(1);
2355+
expect(instance._hiPriInProgress).toBe(false);
2356+
2357+
const updateCellsToRenderSpy = jest.spyOn(instance, '_updateCellsToRender');
2358+
const computeCellsUpdateSpy = jest.spyOn(
2359+
VirtualizedList,
2360+
'_computeCellsUpdate',
2361+
);
2362+
componentDidUpdateSpy.mockClear();
2363+
await act(() => {
2364+
simulateScroll(component, {x: 0, y: 0});
2365+
for (let i = 0; i < items.length; i++) {
2366+
instance._updateCellsToRender();
2367+
}
2368+
});
2369+
2370+
expect(
2371+
computeCellsUpdateSpy.mock.results.some(({value}) => value == null),
2372+
).toBe(true);
2373+
expect(
2374+
computeCellsUpdateSpy.mock.results.some(({value}) => value != null),
2375+
).toBe(true);
2376+
expect(componentDidUpdateSpy).toHaveBeenCalledTimes(1);
2377+
expect(updateCellsToRenderSpy).toHaveBeenCalledTimes(items.length + 1);
2378+
expect(instance._hiPriInProgress).toBe(false);
2379+
computeCellsUpdateSpy.mockRestore();
2380+
2381+
let reenteredFromOnEndReached = false;
2382+
onEndReached.mockImplementationOnce(() => {
2383+
reenteredFromOnEndReached = true;
2384+
simulateScroll(component, {x: 0, y: 0});
2385+
expect(instance._hiPriInProgress).toBe(true);
2386+
});
2387+
2388+
const setState = VirtualizedList.prototype.setState.bind(instance);
2389+
setStateSpy.mockRestore();
2390+
jest.spyOn(instance, 'setState').mockImplementation((updater, onComplete) => {
2391+
setState(
2392+
updater,
2393+
onComplete == null
2394+
? undefined
2395+
: () => {
2396+
onComplete();
2397+
if (
2398+
reenteredFromOnEndReached &&
2399+
instance.state.cellsAroundViewport.last === items.length - 1
2400+
) {
2401+
// The old completion must not release the new update queued
2402+
// by onEndReached during componentDidUpdate.
2403+
expect(instance._hiPriInProgress).toBe(true);
2404+
instance._updateCellsToRender();
2405+
expect(instance._hiPriInProgress).toBe(true);
2406+
}
2407+
},
2408+
);
2409+
});
2410+
2411+
await act(() => {
2412+
simulateScroll(component, {x: 0, y: 80});
2413+
for (let i = 0; i < items.length; i++) {
2414+
instance._updateCellsToRender();
2415+
}
2416+
});
2417+
2418+
expect(reenteredFromOnEndReached).toBe(true);
2419+
expect(instance._hiPriInProgress).toBe(false);
23172420
});
23182421

23192422
it('renders windowSize derived region in middle', async () => {

0 commit comments

Comments
 (0)