Description
On the New Architecture (iOS), RCTScrollViewComponentView.prepareForRecycle resets the native contentInset to UIEdgeInsetsZero. When that recycled native view is later reused by a different ScrollView component whose contentInset prop is non-zero but equal to the previous component's contentInset, the inset is never restored: updateProps: only re-applies contentInset when the value changed (old != new), or on a centerContent true→false transition. Neither condition holds, so the new ScrollView ends up with a native contentInset of zero while its props say otherwise. Content is displaced to the top and the inset region becomes unreachable.
This is a follow-on gap from the #55090 fix (#56832): that PR restores the inset for the centerContent path, but not for the general case of a plain, stable contentInset.
Lineage
Root cause (verifiable on main, no repro needed)
packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm:
prepareForRecycle zeroes the inset:
// Reset contentInset to prevent stale insets leaking into recycled scroll views.
_scrollView.contentInset = UIEdgeInsetsZero;
updateProps: only restores it on a centerContent transition or when the value changed:
// When disabling centerContent, reset inset to prop value
if (oldScrollViewProps.centerContent && !newScrollViewProps.centerContent) {
_scrollView.contentInset = RCTUIEdgeInsetsFromEdgeInsets(newScrollViewProps.contentInset);
}
// Only apply contentInset from props if centerContent is disabled
if (oldScrollViewProps.contentInset != newScrollViewProps.contentInset && !newScrollViewProps.centerContent) {
_scrollView.contentInset = RCTUIEdgeInsetsFromEdgeInsets(newScrollViewProps.contentInset);
}
For a recycled view reused by a component with the same non-zero contentInset and no centerContent:
prepareForRecycle has set the native inset to 0.
- The recycled view still holds the previous component's props, so
oldScrollViewProps.contentInset == newScrollViewProps.contentInset → the old != new branch is skipped.
centerContent never transitions → that branch is skipped too.
Result: the native inset stays at 0, out of sync with the props.
Confirmation of the mechanism
A workaround that nudges contentInset by 1px for the first committed frame (forcing old != new so updateProps re-applies the inset) fully resolves the symptom. This only works if the old == new diff-skip is the cause, confirming the analysis.
Proposed fix
Restore the inset from props in prepareForRecycle instead of zeroing it, mirroring the contentOffset line directly above (which already restores from props.contentOffset). Guard so the centerContent path from #56832 is left untouched:
// Restore from props rather than zeroing: updateProps only re-applies contentInset
// when old != new, so a recycled view reused by a component with the same inset would
// otherwise keep this zeroed value.
_scrollView.contentInset = RCTUIEdgeInsetsFromEdgeInsets(props.contentInset);
React Native version
Reproduced on 0.85.3; confirmed still present on main by source inspection. Platform: iOS. Architecture: New (Fabric).
Steps to reproduce
This reliably reproduces with react-native-collapsible-tab-view on iOS + RN 0.85 (New Architecture). On iOS the library positions tab content using the native contentInset (header height + tab-bar height) with an initial contentOffset of -contentInset. With lazy tabs:
- Open a screen with a collapsible tab view (shared header → every tab uses the same
contentInset).
- Switch to a tab that wasn't mounted yet. Its
ScrollView/SectionList/FlatList mounts and reuses a recycled native scroll view from the pool.
- Because every tab shares the same header height, the recycled view's retained
contentInset equals the new tab's → updateProps skips re-applying it → native inset stays 0.
Expected: the tab scrolls normally; the collapsing header can be revealed by scrolling up.
Actual: the list is jammed at the top — the missing inset means the scroll view has no room above the content, so it clamps to offset 0 and the header can never be revealed again.
Why no minimal library-free repro is attached
The bug requires the recycle pool to hand the new component a view whose retained contentInset equals the new component's non-zero inset. The library's shared-header lazy tabs arrange this naturally (all tabs share one inset value). In a synthetic two-ScrollView sample we were not able to reliably force the pool to reuse a same-inset view, so the symptom did not surface there. The source analysis above is exact and independently verifiable, and the library case reproduces deterministically.
React Native Version
0.85.3
Affected Platforms
Runtime - iOS
Areas
Fabric - The New Renderer
Output of npx @react-native-community/cli info
System:
OS: macOS 26.5.1
CPU: (12) arm64 Apple M2 Pro
Memory: 255.45 MB / 16.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 24.14.0
path: ~/.volta/tools/image/node/24.14.0/bin/node
Yarn:
version: 4.16.0
path: ~/.volta/tools/image/yarn/4.16.0/bin/yarn
npm:
version: 11.9.0
path: ~/.volta/tools/image/node/24.14.0/bin/npm
Watchman:
version: 2025.06.30.00
path: /opt/homebrew/bin/watchman
Managers:
CocoaPods: Not Found
SDKs:
iOS SDK:
Platforms:
- DriverKit 25.5
- iOS 26.5
- macOS 26.5
- tvOS 26.5
- visionOS 26.5
- watchOS 26.5
Android SDK:
API Levels:
- "28"
- "30"
- "31"
- "33"
- "34"
- "35"
- "36"
- "36"
Build Tools:
- 30.0.3
- 33.0.0
- 33.0.1
- 34.0.0
- 35.0.0
- 36.0.0
- 37.0.0
System Images:
- android-34-ext10 | Google Play Intel x86_64 Atom
- android-34 | Google APIs ARM 64 v8a
- android-34 | Google Play ARM 64 v8a
- android-36.1 | Google Play ARM 64 v8a
Android NDK: Not Found
IDEs:
Android Studio: 2026.1 AI-261.23567.138.2611.15503007
Xcode:
version: 26.5/17F42
path: /usr/bin/xcodebuild
Languages:
Java:
version: 17.0.14
path: /opt/homebrew/opt/openjdk@17/bin/javac
Ruby:
version: 3.3.10
path: /Users/anders/.rbenv/shims/ruby
npmPackages:
"@react-native-community/cli":
installed: 20.1.3
wanted: ~20.1.3
react:
installed: 19.2.3
wanted: 19.2.3
react-native:
installed: 0.85.3
wanted: patch:react-native@npm%3A0.85.3#~/.yarn/patches/react-native-npm-0.85.3-2292697f2f.patch
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: true
newArchEnabled: true
iOS:
hermesEnabled: true
newArchEnabled: true
Stacktrace or Logs
See the description and reproduction.
MANDATORY Reproducer
https://github.com/gimi-anders/rn-contentinset-recycle-repro
Screenshots and Videos
No response
Description
On the New Architecture (iOS),
RCTScrollViewComponentView.prepareForRecycleresets the nativecontentInsettoUIEdgeInsetsZero. When that recycled native view is later reused by a differentScrollViewcomponent whosecontentInsetprop is non-zero but equal to the previous component'scontentInset, the inset is never restored:updateProps:only re-appliescontentInsetwhen the value changed (old != new), or on acenterContenttrue→false transition. Neither condition holds, so the newScrollViewends up with a nativecontentInsetof zero while its props say otherwise. Content is displaced to the top and the inset region becomes unreachable.This is a follow-on gap from the #55090 fix (#56832): that PR restores the inset for the
centerContentpath, but not for the general case of a plain, stablecontentInset.Lineage
UIEdgeInsetsZeroresetcenterContenttrue→false pathRoot cause (verifiable on
main, no repro needed)packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm:prepareForRecyclezeroes the inset:// Reset contentInset to prevent stale insets leaking into recycled scroll views. _scrollView.contentInset = UIEdgeInsetsZero;updateProps:only restores it on acenterContenttransition or when the value changed:For a recycled view reused by a component with the same non-zero
contentInsetand nocenterContent:prepareForRecyclehas set the native inset to0.oldScrollViewProps.contentInset == newScrollViewProps.contentInset→ theold != newbranch is skipped.centerContentnever transitions → that branch is skipped too.Result: the native inset stays at
0, out of sync with the props.Confirmation of the mechanism
A workaround that nudges
contentInsetby 1px for the first committed frame (forcingold != newsoupdatePropsre-applies the inset) fully resolves the symptom. This only works if theold == newdiff-skip is the cause, confirming the analysis.Proposed fix
Restore the inset from props in
prepareForRecycleinstead of zeroing it, mirroring thecontentOffsetline directly above (which already restores fromprops.contentOffset). Guard so thecenterContentpath from #56832 is left untouched:React Native version
Reproduced on
0.85.3; confirmed still present onmainby source inspection. Platform: iOS. Architecture: New (Fabric).Steps to reproduce
This reliably reproduces with
react-native-collapsible-tab-viewon iOS + RN 0.85 (New Architecture). On iOS the library positions tab content using the nativecontentInset(header height + tab-bar height) with an initialcontentOffsetof-contentInset. Withlazytabs:contentInset).ScrollView/SectionList/FlatListmounts and reuses a recycled native scroll view from the pool.contentInsetequals the new tab's →updatePropsskips re-applying it → native inset stays0.Expected: the tab scrolls normally; the collapsing header can be revealed by scrolling up.
Actual: the list is jammed at the top — the missing inset means the scroll view has no room above the content, so it clamps to offset 0 and the header can never be revealed again.
Why no minimal library-free repro is attached
The bug requires the recycle pool to hand the new component a view whose retained
contentInsetequals the new component's non-zero inset. The library's shared-header lazy tabs arrange this naturally (all tabs share one inset value). In a synthetic two-ScrollViewsample we were not able to reliably force the pool to reuse a same-inset view, so the symptom did not surface there. The source analysis above is exact and independently verifiable, and the library case reproduces deterministically.React Native Version
0.85.3
Affected Platforms
Runtime - iOS
Areas
Fabric - The New Renderer
Output of
npx @react-native-community/cli infoStacktrace or Logs
MANDATORY Reproducer
https://github.com/gimi-anders/rn-contentinset-recycle-repro
Screenshots and Videos
No response