Repository navigation
SectionList with sticky section headers jumps back and forth #58925
Description
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:
ScrollViewStickyHeaderforwards its layout tochild.props.onLayout, and clones the child withonLayout: undefined. But the childrenVirtualizedListpasses areCellRendererelements, which seem to takeonCellLayoutrather thanonLayout. If so, the cell keeps its ownonLayoutand measures itself relative to the sticky wrapper (y = 0), andListMetricsAggregator.notifyCellLayoutstores that as the header's offset. The comment inVirtualizedListCellRenderer("when this is a sticky header,onLayoutwill get automatically extracted and called explicitly byScrollViewStickyHeader") suggests the forwarding was meant to happen.If section headers are recorded at offset 0, that could explain the jumping.
VirtualizedListsizes each spacer aslastMetrics.offset + lastMetrics.length - firstMetrics.offset. Once the render window leaves the always-renderedinitialNumToRenderregion, a spacer starts at cellinitialNumToRender. 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.elementsThatOverlapOffsetsalso 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
StickyHeaderComponentthat forwards the sticky wrapper's layout toonCellLayoutand removesonCellLayoutfrom the cloned child. It stops the jumping. If the cause is right, a fix inScrollViewStickyHeadercould 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
CellRendererto acceptonLayoutagain, so the existing extraction works as the comment describes.
Steps to reproduce
- Scroll down.
- 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
Activity