Skip to content

Fix disappearing cards in masonry when optimizeItemArrangement is dis…#2104

Closed
j0sip wants to merge 2 commits into
Shopify:mainfrom
j0sip:2103-fix-disappearing-cards
Closed

Fix disappearing cards in masonry when optimizeItemArrangement is dis…#2104
j0sip wants to merge 2 commits into
Shopify:mainfrom
j0sip:2103-fix-disappearing-cards

Conversation

@j0sip

@j0sip j0sip commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes (issue #2103)

This PR fixes an issue in masonry layout where cards unexpectedly disappear as user scrolls when optimizeItemArrangement is disabled.

It adds a new arrays of sorted layout indices to MasonryLayoutManager, which enables correct binary search when calculating visible items. Current implementation of binary search fails when optimizeItemArrangement is disabled, because the array of layouts is not sorted by y position. This leads to incorrect results when column heights are different.

The fix only affects masonry layout and only when optimizeItemArrangement === false, since the issue is not occurring when the prop is enabled.

Reviewers’ hat-rack 🎩

  • [ ]

Screenshots or videos (if needed)

Video (after disabling optimizeItemArrangement in ComplexMasonry from fixture app:

flashlist-fix.mp4

@naqvitalha

naqvitalha commented Feb 26, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR! Binary search on the flat layouts array breaks in masonry with sequential placement because items zigzag between columns (e.g., y-values go [0, 0, 100, 50, 200, 100]), so binary search can miss visible items.

However, the sorting approach adds significant overhead:

  • O(N log N) sort on every layout change (height measurements, new items)
  • O(N) extra memory for sorted copies
  • Rebuild on every processLayoutInfo and recomputeLayouts call

This is avoidable because items within each column are naturally sorted by y. With sequential placement, item i belongs to column i % maxColumns. So you can binary search per column instead of sorting the whole array:

getVisibleLayouts(start: number, end: number): ConsecutiveNumbers {
  if (this.optimizeItemArrangement) {
    return super.getVisibleLayouts(start, end);
  }

  let minIdx = Infinity, maxIdx = -1;

  for (let col = 0; col < this.maxColumns; col++) {
    // Items in column: col, col+maxColumns, col+2*maxColumns, ...
    // Their y-values are sorted — binary search works directly
    const first = this.binarySearchColumn(col, start, true);
    const last = this.binarySearchColumn(col, end, false);
    if (first !== -1) minIdx = Math.min(minIdx, first);
    if (last !== -1) maxIdx = Math.max(maxIdx, last);
  }

  return minIdx > maxIdx
    ? ConsecutiveNumbers.EMPTY
    : new ConsecutiveNumbers(minIdx, maxIdx);
}

private binarySearchColumn(
  col: number, threshold: number, findFirst: boolean
): number {
  // Items in this column at original indices: col, col+mc, col+2*mc, ...
  const mc = this.maxColumns;
  const count = Math.ceil((this.layouts.length - col) / mc);
  let left = 0, right = count - 1, result = -1;

  while (left <= right) {
    const mid = (left + right) >> 1;
    const idx = col + mid * mc;
    const layout = this.layouts[idx];
    const pos = this.horizontal ? layout.x : layout.y;
    const size = this.horizontal ? layout.width : layout.height;

    if (findFirst) {
      if (pos + size > threshold) { result = idx; right = mid - 1; }
      else { left = mid + 1; }
    } else {
      if (pos <= threshold) { result = idx; left = mid + 1; }
      else { right = mid - 1; }
    }
  }
  return result;
}

@naqvitalha naqvitalha closed this Mar 3, 2026
@nameishuy

Copy link
Copy Markdown

Hi, I stuck with this issue and patch package these codes in. But it still disappearing, I tried console log in getVisibleLayouts but they didnt trigger

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.

Disappearing items in Masonry layout

3 participants