Skip to content

Stale ViewHolder onLayout can throw when its layout index no longer exists #2291

Description

@KJ21-ENG

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:

  1. Render a FlashList with enough items to create/recycle multiple ViewHolders.
  2. Trigger a data change that reduces or replaces the layout table.
  3. Immediately navigate away from the screen or otherwise transition the list while item layout callbacks can still be delivered.
  4. A stale ViewHolder.onLayout calls onSizeChanged(index, size).
  5. validateItemSize() calls getLayout(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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Important but not urgentagent-triageTrigger agent triage on this issue

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions