Stale ViewHolder onLayout can throw when its layout index no longer exists
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 7.2k
- Forks
- 393
- Avg merge
- 1d 3h
- Merged PRs (30d)
- 1
Description
Description
ViewHolder.onLayout can report an item size after the list's data/layout table has already changed. When that happens, validateItemSize() still uses the render-time index captured by the ViewHolder, and currently reads the stored layout with recyclerViewManager.getLayout(index).
If the stale index is no longer present in the layout manager, getLayout(index) throws index out of bounds, not enough layouts, which can crash the app during fast list transitions or navigation away from a screen.
The relevant current code path is:
validateItemSize()callsrecyclerViewManager.getLayout(index):
https://github.com/Shopify/flash-list/blob/f4278ba0e0aa0da4f8b53035350fc5e063078a54/src/recyclerview/RecyclerView.tsx#L374-L377tryGetLayout()already exists and safely returnsundefinedwhen the index is outside the current layout table:
https://github.com/Shopify/flash-list/blob/f4278ba0e0aa0da4f8b53035350fc5e063078a54/src/recyclerview/RecyclerViewManager.ts#L138-L153LayoutManager.getLayout()throws whenindex >= this.layouts.length:
https://github.com/Shopify/flash-list/blob/f4278ba0e0aa0da4f8b53035350fc5e063078a54/src/recyclerview/layout-managers/LayoutManager.ts#L230-L233
Current behavior
When an obsolete ViewHolder layout callback runs after the underlying FlashList layouts have changed, validateItemSize() calls:
const layout = recyclerViewManager.getLayout(index);
If that render-time index no longer exists, LayoutManager.getLayout() throws:
index out of bounds, not enough layouts
This can happen during fast data changes or screen transitions where React Native/native layout callbacks from old cells arrive after FlashList has already updated the layout table.
Expected behavior
Stale layout callbacks for indexes that no longer exist should be ignored. Current indexes should continue through the existing width/height validation.
A minimal defensive change is to use tryGetLayout(index) in validateItemSize():
const layout = recyclerViewManager.tryGetLayout(index);
if (layout === undefined) {
return;
}
That preserves the existing validation for current layouts while avoiding a crash for obsolete measurements.
Reproduction
Expo Snack or minimal reproduction link:
I do not have a standalone Expo Snack yet. This was observed in an app flow where an inverted FlashList report/chat list is navigated away from immediately after its data changes, allowing a stale native onLayout callback from an old ViewHolder to arrive after the list's layout table has already changed.
Reduced sequence:
- Render a FlashList with enough items to create/recycle multiple
ViewHolders. - Trigger a data change that reduces or replaces the layout table.
- Immediately navigate away from the screen or otherwise transition the list while item layout callbacks can still be delivered.
- A stale
ViewHolder.onLayoutcallsonSizeChanged(index, size). validateItemSize()callsgetLayout(index)for an index that no longer exists and throws.
Platform
- iOS
- Android
- Web (if applicable)
Environment
React Native info output:
Not available from the reduced upstream report yet.
FlashList version: 2.3.0. The same validateItemSize() / getLayout(index) code path also appears to exist on current main at the links above.
Additional context
We are carrying a local package patch with the tryGetLayout(index) guard described above, and it prevents this crash without changing the validation behavior for active/current item indexes.
Checklist
- I've searched existing issues and couldn't find a duplicate
- I've provided a minimal reproduction (Expo Snack preferred)
- I'm using the latest version of @shopify/flash-list
- I've included all required information above
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/recyclerview/RecyclerView.tsx at validateItemSize() and compare its getLayout(index) call with tryGetLayout() in src/recyclerview/RecyclerViewManager.ts. Verify that an absent layout is ignored while current indexes retain the existing width and height validation, then run the relevant project tests or checks.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react-native, typescript
- Domain
- mobile
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100