Skip to content

fix(recycling): skip render stack entries without a layout - #2514

Open
teoarjun wants to merge 1 commit into
Shopify:mainfrom
teoarjun:fix/viewholder-collection-layout-bounds
Open

teoarjun wants to merge 1 commit into
Shopify:mainfrom
teoarjun:fix/viewholder-collection-layout-bounds

Conversation

@teoarjun

@teoarjun teoarjun commented Sep 21, 2026 •

Copy link
Copy Markdown

Description

Fixes #2440

ViewHolderCollection reads every render stack entry through the throwing getLayout accessor, so an entry that outlives a layout-table shrink crashes the render with index out of bounds, not enough layouts.

RecyclerViewManager.modifyChildrenLayout truncates the layout table on its first line, but the render stack is only pruned on some of its exit paths. When the engaged range is unchanged, recomputeEngagedIndices() returns undefined and RenderStackManager.sync() never runs against the new data length, leaving keys that point past the last layout.

This routes the render path through the existing tryGetLayout accessor and skips entries that have no layout, matching the bounds-safe pattern StickyHeaders already uses. Stale entries are dropped until the next sync restores the pairing.

Reviewers' hat-rack 🎩

  • The guard sits in the render loop rather than at the source. Pruning the render stack inside modifyChildrenLayout would address the stale state directly, but reaches into the engaged-indices path — happy to take that route instead if you'd prefer it.
  • src/__tests__/ViewHolderCollection.test.tsx is new. It renders a stack holding indices 0-24 against a layout table truncated to 10 and asserts only the 10 live entries render; it fails without the fix, rendering all 25.

Verified locally on the branch: 201 tests pass across 16 suites, tsc --noEmit clean, eslint clean.

Screenshots or videos (if needed)

Not applicable — no visual change; the fix prevents a render-time crash.

ViewHolderCollection reads every render stack entry through the throwing
getLayout accessor, so an entry that outlives a layout-table shrink crashes
the render with "index out of bounds, not enough layouts".

modifyChildrenLayout truncates the layout table on its first line, but the
render stack is only pruned on some of its exit paths. When the engaged
range is unchanged, recomputeEngagedIndices returns undefined and sync()
never runs against the new data length, leaving keys that point past the
last layout.

Read layouts through tryGetLayout and skip the entries that have none,
matching the bounds-safe pattern StickyHeaders already uses. Stale entries
are dropped until the next sync restores the pairing.

Fixes Shopify#2440
@teoarjun
teoarjun marked this pull request as ready for review September 25, 2026 11:17
@teoarjun

teoarjun commented Sep 25, 2026 •

Copy link
Copy Markdown
Author

I have signed the CLA!

This branch has not been deployed

No deployments
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.

ViewHolderCollection render throws "index out of bounds, not enough layouts" when the render stack outlives a layout-table shrink

1 participant