Shopify / Shopify/flash-list

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

Open
#2,086 7 comments 3 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug P0
Dominant language
TypeScript
Stars
7.2k
Forks
393
Avg merge
1d 3h
Merged PRs (30d)
1

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reproducing the crash with the supplied PagerFlashList example under Android, Fabric, edge-to-edge, and the listed React Native and library versions. Exercise rapid pin/unpin reordering while scrolling, then trace the view traversal failure; done means the repro no longer crashes and the relevant regression coverage passes.

Written by the indexing model from the issue text.

Assessment

Tech stack
react-native, typescript
Domain
mobile
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.