Skip to content

FlashList Android crash with Fabric + Edge-to-Edge during rapid data reordering #2086

@Jianlong-Nie

Description

@Jianlong-Nie

We are experiencing a reproducible Android crash when using FlashList with the New Architecture (Fabric enabled) under rapid data reordering while scrolling.

This issue occurs when the user pins or unpins a post and then quickly scrolls the list.

The crash happens on the UI thread during view traversal, with the following exception:

IllegalStateException: EdgeToEdgeReactViewGroup contains null child at index X
when traversal in dispatchGetDisplayList, the view may have been removed.

This issue only occurs on Android, only with Fabric enabled

Environment
• Library: "@shopify/flash-list": "2.2.0",
• Platform: Android
• Architecture: New Architecture (Fabric enabled)
• Edge-to-Edge: Enabled
• React Native: 0.81.4 /~54.0.31 (Fabric ON)
• Reanimated: 4.1.3
• Gesture Handler: ~2.28.0

import { FlashList, FlashListProps, FlashListRef } from "@shopify/flash-list"
import React, { memo, ReactElement, RefObject, useMemo } from "react"
import { type ViewToken } from "react-native"
import Animated, { AnimatedProps } from "react-native-reanimated"

import { RefreshComponent } from "src/components/atoms/feed/RefreshComponent"
import { usePagerContext } from "src/components/profile/tab-view/PagerContext"
import { useRefresh } from "src/hooks/app/useRefresh"
import { useFindScrollViewTag } from "src/hooks/scroll/useFindScrollViewTag"

type PagerFlashListProps<T> = Omit<
  FlashListProps<T>,
  | "onMomentumScrollBegin" // Use ScrollContext instead.
  | "onMomentumScrollEnd" // Use ScrollContext instead.
  | "onScroll" // Use ScrollContext instead.
  | "onScrollBeginDrag" // Use ScrollContext instead.
  | "onScrollEndDrag" // Use ScrollContext instead.
  | "refreshControl" // Pass refreshing and/or onRefresh instead.
  | "contentOffset" // Pass headerOffset instead.
  | "progressViewOffset" // Can't be an animated value
  | "onRefresh"
  | "refreshing"
> & {
  readonly onRefresh?: () => Promise<unknown>
  readonly onItemSeen?: (item: T) => void
  readonly progressViewOffset?: number
}

const AnimatedFlashList = Animated.createAnimatedComponent(FlashList) as <T>(
  props: AnimatedProps<
    FlashListProps<T> & {
      ref?: React.Ref<FlashListRef<T>> | undefined
    }
  >
) => ReactElement

const PagerFlashList_Unmemo = <T,>({
  onRefresh: _onRefresh,
  onItemSeen,
  style,
  progressViewOffset,
  automaticallyAdjustsScrollIndicatorInsets = false,
  ...props
}: PagerFlashListProps<T>) => {
  const {
    scrollHandler,
    isFocused,
    setScrollViewTag,
    scrollElRef: ref,
    headerHeight: headerOffset
  } = usePagerContext()

  useFindScrollViewTag({
    isFocused,
    scrollRef: ref,
    setScrollViewTag
  })

  const [onViewableItemsChanged, viewabilityConfig] = useMemo(() => {
    if (!onItemSeen) {
      return [undefined, undefined]
    }
    return [
      (info: { viewableItems: ViewToken<T>[]; changed: ViewToken<T>[] }) => {
        for (const item of info.changed) {
          if (item.isViewable) {
            onItemSeen(item.item)
          }
        }
      },
      {
        itemVisiblePercentThreshold: 40,
        minimumViewTime: 0.5e3
      }
    ]
  }, [onItemSeen])

  const { onRefresh, isRefreshing } = useRefresh({ refetch: _onRefresh })

  return (
    <AnimatedFlashList
      onViewableItemsChanged={onViewableItemsChanged}
      showsVerticalScrollIndicator={false}
      viewabilityConfig={viewabilityConfig}
      {...props}
      automaticallyAdjustsScrollIndicatorInsets={
        automaticallyAdjustsScrollIndicatorInsets
      }
      contentContainerStyle={{
        paddingTop: headerOffset,
        paddingBottom: headerOffset * 1.5
      }}
      maintainVisibleContentPosition={{ disabled: true }}
      onScroll={scrollHandler}
      ref={ref as RefObject<FlashListRef<T> | null> | undefined}
      refreshControl={
        _onRefresh ? (
          <RefreshComponent
            onRefresh={onRefresh}
            progressViewOffset={progressViewOffset ?? headerOffset}
            refreshing={isRefreshing}
          />
        ) : undefined
      }
      scrollEventThrottle={isFocused ? 16 : undefined}
      scrollsToTop
      style={style}
    />
  )
}

export const PagerFlashList = memo(
  PagerFlashList_Unmemo
) as typeof PagerFlashList_Unmemo

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0bugSomething isn't working

    Type

    No type
    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