Skip to content

fix(layout): recompute the tail of horizontal lists on the scroll axis - #2496

Open
ahmdshrif wants to merge 1 commit into
Shopify:mainfrom
ahmdshrif:fix-horizontal-tail-layout-sync
Open

fix(layout): recompute the tail of horizontal lists on the scroll axis#2496
ahmdshrif wants to merge 1 commit into
Shopify:mainfrom
ahmdshrif:fix-horizontal-tail-layout-sync

Conversation

@ahmdshrif

Copy link
Copy Markdown

Description

RVLayoutManager._recomputeLayouts caps each recompute pass at maxItemsToProcess items. When the pass ends before the last item, it checks whether the items it just moved now sit past the untouched tail, and if so recomputes the tail too:

const lastIndex = this.layouts.length - 1;
// Since layout managers derive height from last indices we need to make
// sure they're not too much out of sync.
if (this.layouts[lastIndex].y < this.layouts[endIndex].y) {
  this.recomputeLayouts(this.lastSkippedLayoutIndex, lastIndex);
  this.lastSkippedLayoutIndex = Number.MAX_VALUE;
}

That check reads y unconditionally. In a horizontal list RVLinearLayoutManagerImpl.recomputeLayouts assigns layout.y = 0 for every item and advances layout.x instead, so the comparison is always 0 < 0 and the tail is never recomputed. The guard is dead code on the horizontal path.

Two things go wrong as a result:

  • The tail overlaps the items that were just moved. Items past the cap keep the x they were given by the previous, smaller estimates, so they land on top of much earlier items. That corrupts findFirstVisibleIndex/findLastVisibleIndex (both binary-search the layout array assuming it is monotonic on the scroll axis), so the wrong items are rendered and recycled.
  • getLayoutSize() reports a stale content width. RVLinearLayoutManagerImpl.getLayoutSize derives the horizontal size from lastLayout.x + lastLayout.width, which is exactly the value the guard exists to keep fresh. The list reports a content width several times smaller than the real one, which breaks scroll extent, scrollToEnd and end-reached detection until a later layout pass happens to sweep the tail back in.

On a 300-item horizontal list where the first 25 measured items come back wider than the estimate, the recompute pass covers 0–274 and then stops:

item 274 x item 275 x item 299 x reported content width
before 274000 55000 59800 60800
after 274000 275000 299000 300000

The equivalent vertical list is already correct today — this is purely the horizontal path.

Fix

Compare on the axis the list actually grows along:

private getScrollAxisPosition(layout: RVLayout): number {
  return this.horizontal ? layout.x : layout.y;
}

horizontal is already the base class's own notion of the scroll axis, so no subclass needs to change. Grid and masonry both lay out along y, so their behaviour is byte-identical.

Reviewers' hat-rack 🎩

  • The new getScrollAxisPosition helper is the only behavioural change; everything else is a comment update.
  • LinearLayoutManager.test.ts gains a Tail sync after a partial recompute block with a vertical and a horizontal case. The vertical one passes both before and after the fix — it is there to show the assertions describe existing intended behaviour, not new behaviour. The horizontal one fails on main with Expected: >= 274000 / Received: 55000 and passes with the fix.
  • Full suite: 14 suites / 189 tests passing. tsc --noEmit and eslint clean.

_recomputeLayouts guards against a partial recompute leaving the tail
behind by comparing the last item's position with the end of the range
it just processed. It read y unconditionally, but horizontal linear
layouts keep y at 0 for every item and advance x, so the comparison was
always 0 < 0 and the guard never fired.

The tail then kept positions derived from stale estimates: it overlapped
the items that had just been moved, which breaks the binary searches in
findVisibleIndex, and getLayoutSize() reported a content width taken
from that stale last item.

Compare on the scroll axis instead - x when horizontal, y otherwise.
Grid and masonry lay out along y, so they are unaffected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant