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() calls recyclerViewManager.getLayout(index):
|
const validateItemSize = useCallback( |
|
(index: number, size: RVDimension) => { |
|
const layout = recyclerViewManager.getLayout(index); |
|
const width = Math.max( |
tryGetLayout() already exists and safely returns undefined when the index is outside the current layout table:
|
getLayout(index: number) { |
|
if (!this.layoutManager) { |
|
throw new Error(ErrorMessages.layoutManagerNotInitializedLayoutInfo); |
|
} |
|
return this.layoutManager.getLayout(index); |
|
} |
|
|
|
tryGetLayout(index: number) { |
|
if ( |
|
this.layoutManager && |
|
index >= 0 && |
|
index < this.layoutManager.getLayoutCount() |
|
) { |
|
return this.layoutManager.getLayout(index); |
|
} |
|
return undefined; |
LayoutManager.getLayout() throws when index >= this.layouts.length:
|
getLayout(index: number): RVLayout { |
|
if (index >= this.layouts.length) { |
|
throw new Error(ErrorMessages.indexOutOfBounds); |
|
} |
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.onLayout calls onSizeChanged(index, size).
validateItemSize() calls getLayout(index) for an index that no longer exists and throws.
Platform
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
Description
ViewHolder.onLayoutcan report an item size after the list's data/layout table has already changed. When that happens,validateItemSize()still uses the render-timeindexcaptured by theViewHolder, and currently reads the stored layout withrecyclerViewManager.getLayout(index).If the stale index is no longer present in the layout manager,
getLayout(index)throwsindex 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):flash-list/src/recyclerview/RecyclerView.tsx
Lines 374 to 377 in f4278ba
tryGetLayout()already exists and safely returnsundefinedwhen the index is outside the current layout table:flash-list/src/recyclerview/RecyclerViewManager.ts
Lines 138 to 153 in f4278ba
LayoutManager.getLayout()throws whenindex >= this.layouts.length:flash-list/src/recyclerview/layout-managers/LayoutManager.ts
Lines 230 to 233 in f4278ba
Current behavior
When an obsolete
ViewHolderlayout callback runs after the underlying FlashList layouts have changed,validateItemSize()calls:If that render-time
indexno longer exists,LayoutManager.getLayout()throws: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)invalidateItemSize():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
onLayoutcallback from an oldViewHolderto arrive after the list's layout table has already changed.Reduced sequence:
ViewHolders.ViewHolder.onLayoutcallsonSizeChanged(index, size).validateItemSize()callsgetLayout(index)for an index that no longer exists and throws.Platform
Environment
React Native info output:
FlashList version: 2.3.0. The same
validateItemSize()/getLayout(index)code path also appears to exist on currentmainat 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