Skip to content

SectionList with sticky section headers jumps back and forth #58925

Description

@hzr

Description

In a long SectionList with stickySectionHeadersEnabled, the list can break once you've scrolled down a bit. It starts jumping back and forth, flipping between two sets of rows.

Claude found what appears to be a fix by patching ScrollViewStickyHeader. This can be flipped on in the reproducer app.

The following is Claude's analysis. I have not verified if any of this is true:

ScrollViewStickyHeader forwards its layout to child.props.onLayout, and clones the child with onLayout: undefined. But the children VirtualizedList passes are CellRenderer elements, which seem to take onCellLayout rather than onLayout. If so, the cell keeps its own onLayout and measures itself relative to the sticky wrapper (y = 0), and ListMetricsAggregator.notifyCellLayout stores that as the header's offset. The comment in VirtualizedListCellRenderer ("when this is a sticky header, onLayout will get automatically extracted and called explicitly by ScrollViewStickyHeader") suggests the forwarding was meant to happen.

If section headers are recorded at offset 0, that could explain the jumping. VirtualizedList sizes each spacer as lastMetrics.offset + lastMetrics.length - firstMetrics.offset. Once the render window leaves the always-rendered initialNumToRender region, a spacer starts at cell initialNumToRender. When that cell is a section header, the spacer would come out too tall by the header's real offset. The rendered rows would then move down, the window would be recomputed as if you were further up and reattach to the initial region, the spacer would disappear, and the cycle would repeat. elementsThatOverlapOffsets also assumes offsets increase, so a header at 0 could send its binary search the wrong way for rows just before a header.

The workaround in the repro is a StickyHeaderComponent that forwards the sticky wrapper's layout to onCellLayout and removes onCellLayout from the cloned child. It stops the jumping. If the cause is right, a fix in ScrollViewStickyHeader could look like this:

--- a/packages/react-native/Libraries/Components/ScrollView/ScrollViewStickyHeader.js
+++ b/packages/react-native/Libraries/Components/ScrollView/ScrollViewStickyHeader.js
@@ const _onLayout = (event: LayoutChangeEvent) => {
     if (child.props.onLayout) {
       child.props.onLayout(event);
     }
+    // VirtualizedList cells take `onCellLayout` rather than `onLayout`.
+    if (child.props.onCellLayout) {
+      child.props.onCellLayout(event, child.props.cellKey, child.props.index);
+    }
   };
@@
       {cloneElement(child, {
         onLayout: undefined, // we call this manually through our this._onLayout
+        onCellLayout: undefined, // likewise, for VirtualizedList cells
         style: styles.fill, // We transfer the child style to the wrapper.
       })}

Another option would be for CellRenderer to accept onLayout again, so the existing extraction works as the comment describes.

Steps to reproduce

  1. Scroll down.
  2. Around section 10 to 11, the list starts jumping back and forth.

React Native Version

0.86.3

Affected Platforms

Runtime - Android, Runtime - iOS

Output of npx @react-native-community/cli info

System:
  OS: macOS 27.0
  CPU: (10) arm64 Apple M1 Pro
  Memory: 57.91 MB / 32.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 24.14.1
    path: /Users/_/.nvm/versions/node/v24.14.1/bin/node
  Yarn:
    version: 4.13.0
    path: /Users/_/.nvm/versions/node/v24.14.1/bin/yarn
  npm:
    version: 11.11.0
    path: /Users/_/.nvm/versions/node/v24.14.1/bin/npm
  Watchman: Not Found
Managers:
  CocoaPods:
    version: 1.17.0
    path: /opt/homebrew/bin/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 27.0
      - iOS 27.0
      - macOS 27.0
      - tvOS 27.0
      - visionOS 27.0
      - watchOS 27.0
  Android SDK: Not Found
IDEs:
  Android Studio: 2025.3 AI-253.31033.145.2533.15113396
  Xcode:
    version: 27.0/27A266a
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 17.0.20.1
    path: /usr/bin/javac
  Ruby:
    version: 4.0.7
    path: /opt/homebrew/bin/ruby
npmPackages:
  "@react-native-community/cli": Not Found
  react:
    installed: 19.2.3
    wanted: 19.2.3
  react-native:
    installed: 0.86.3
    wanted: 0.86.3
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: Not found
  newArchEnabled: Not found
iOS:
  hermesEnabled: Not found
  newArchEnabled: Not found

Stacktrace or Logs

-

MANDATORY Reproducer

https://github.com/hzr/jumping-section-list

Screenshots and Videos

Bug appears at section 10

Untitled.mp4

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions