Skip to content

v2: Android view flattening removes ViewHolderCollection's container, breaking row positions (missing contentContainerStyle padding + ListHeaderComponent offset) #2399

Description

@laurenbell-zoe

Description

FlashList v2 positions every row with position: absolute inside a plain container view
(ViewHolderCollection's CompatView). That container is usually a layout-only view, so React
Native's view flattening (New Architecture / Fabric, historically Android) can remove it from the
native hierarchy. When that happens the absolutely-positioned rows are re-anchored to the
ScrollView's content view and lose two offsets at once:

  • the contentContainerStyle horizontal padding → rows render flush against the screen edge
  • the ListHeaderComponent height → rows overlap the header, and because absolute children
    contribute no size, the container's explicit height also stops contributing to the scrollable
    content, corrupting the scroll range

The outer RecyclerView container already sets collapsable={false}, but the row container does
not. Verified still present in main (src/recyclerview/ViewHolderCollection.tsx, the
<CompatView style={hasData && containerStyle}>).

Current behavior

On Android (frequently) and iOS (rarely — flattening eligibility is re-evaluated per commit, and
the container's opacity: 0 → 1 first-commit workaround masks it most of the time), a mounted
list renders with rows at x = 0 ignoring content padding, first row underneath the
ListHeaderComponent, and a wrong scroll extent.

Evidence from a broken instance (chat UI, Android emulator, RN 0.85 new arch):

  • FlashList's own model is correct: ref.getWindowSize().width = 350 (padded width),
    ref.getFirstItemOffset() = 128.67 (header height), row layouts measured — all sane.
  • The native hierarchy disagrees: rows render at x=0, width 943px (unpadded), first row at
    y=0 overlapping the header, content not scrollable.

Adding collapsable={false} to the ViewHolderCollection container fixes both symptoms
immediately (verified on-device).

Expected behavior

Rows are positioned inside the padded content area, after the ListHeaderComponent, with a correct
scroll range — i.e. the container that anchors the absolute rows is never removed by view
flattening, matching the collapsable={false} already applied to the outer container.

Reproduction

Expo Snack or minimal reproduction link: (minimal component below; note the probabilistic
nature — flattening is decided per commit, so a single mount may render correctly. Remounting
the list repeatedly triggers it reliably on Android in our app.)

const DATA = Array.from({ length: 20 }, (_, i) => `Item ${i}`);

function Repro() {
  const [visible, setVisible] = useState(true);
  return (
    <View style={{ flex: 1 }}>
      <Button title="remount list" onPress={() => { setVisible(false); requestAnimationFrame(() => setVisible(true)); }} />
      {visible && (
        <FlashList
          data={DATA}
          renderItem={({ item }) => <Text style={{ height: 40 }}>{item}</Text>}
          contentContainerStyle={{ paddingHorizontal: 24 }}
          ListHeaderComponent={<View style={{ height: 120 }} />}
        />
      )}
    </View>
  );
}

Platform

  • iOS (reproduces less frequently on iOS)
  • Android
  • Web (if applicable)

Environment

React Native info output:
System:
  OS: macOS 26.5
  CPU: (14) arm64 Apple M3 Max
  Memory: 206.91 MB / 36.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 24.15.0
  Yarn:
    version: 4.17.1
  npm:
    version: 11.12.1
  Watchman:
    version: 2026.01.12.00
Managers:
  CocoaPods:
    version: 1.16.2
SDKs:
  iOS SDK:
    Platforms:
      - iOS 26.2 (and other 26.2 platforms)
  Android SDK: Not Found  # CLI quirk — SDK managed via Android Studio; app builds/runs fine
IDEs:
  Android Studio: 2025.2 AI-252.28238.7.2523.14688667
  Xcode:
    version: 26.3/17C529
Languages:
  Java:
    version: 25.0.2 (Gradle pinned to Temurin 21)
  Ruby:
    version: 3.3.1
npmPackages:
  react:
    installed: 19.2.3
  react-native:
    installed: 0.85.3
Android:
  hermesEnabled: Not found     # Expo-managed project — CLI can't read it; Hermes enabled
  newArchEnabled: Not found    # confirmed true in generated android/gradle.properties
iOS:
  hermesEnabled: Not found
  newArchEnabled: Not found

Expo SDK 56, New Architecture enabled (newArchEnabled=true), Hermes. Reproduced on Android
emulator (Medium Phone, API 36.1) and on iOS (intermittently).

FlashList version: 2.3.1 (bug confirmed still present in 2.3.2 / current main source)

Additional context

  • Root cause matches the documented purpose of collapsable: views that are measured or act as
    coordinate anchors must opt out of flattening (https://reactnative.dev/architecture/view-flattening).
  • The intermittency maps to flattening eligibility toggling across commits: the container renders
    with opacity: 0 on first commit (not layout-only → kept), then opacity: 1 (eligible →
    may be culled on a later commit). Different mount/update sequences → different outcomes.
  • This may be the underlying mechanism behind the known-issues note that FlashList "can't read"
    contentContainerStyle padding, and possibly some Android new-arch mispositioning reports.
  • Workaround we're shipping: a one-line patch adding collapsable: false to the container, plus
    moving horizontal padding onto rows per the docs' recommendation.

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

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions