Skip to content

[iOS] contentInset and contentOffset on recycled ScrollView is not reset on reuse #55090

@cbrevik

Description

@cbrevik

Description

It does not seem like contentInset or contentOffset is reset when prepareForRecyle is run on ScrollViews on iOS for New Architecture.

This is most noticeable when using centerContent={true} on ScrollViews with some height and not much content. If you unmount such a ScrollView, and mount another with small height (like a horizontal ScrollView in my example here), you will notice the content in the horizontal one is displaced quite a bit.

This is because the native RCTEnhancedScrollView is recycled between those mounts, and the inset and offset is re-used (see logs and video). This is not a problem on the old architecture.

If I apply a patch to actually recalculate the content inset on recycle this is fixed: e3e2bb1

Admittedly it is kind of hacky, so do not think this is a good and proper fix.

Steps to reproduce

  1. Use the RNTester-app with this reproducer
  2. Click Playground tab, and see that low-height horizontal ScrollView is at the top (as it should be).
  3. Click Vertical ScrollView With Centered Content-button, and notice that the content of vertical scroll view is at top (and not centered), which is a bug.
  4. Click Low Height Horizontal Scroll-button, and notice that now the low-height horizontal ScrollView is further down on the screen. Also a bug.

React Native Version

0.83.1

Affected Platforms

Runtime - iOS

Areas

Fabric - The New Renderer

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

System:
  OS: macOS 15.6.1
  CPU: (10) arm64 Apple M1 Pro
  Memory: 1.17 GB / 32.00 GB
  Shell:
    version: 4.0.2
    path: /opt/homebrew/bin/fish
Binaries:
  Node:
    version: 22.17.1
    path: /usr/local/bin/node
  Yarn:
    version: 1.22.22
    path: /opt/homebrew/bin/yarn
  npm:
    version: 10.9.2
    path: /usr/local/bin/npm
  Watchman:
    version: 2025.08.11.00
    path: /opt/homebrew/bin/watchman
Managers:
  CocoaPods:
    version: 1.15.2
    path: /Users/cb/.rbenv/shims/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 25.1
      - iOS 26.1
      - macOS 26.1
      - tvOS 26.1
      - visionOS 26.1
      - watchOS 26.1
  Android SDK:
    API Levels:
      - "31"
      - "34"
      - "35"
      - "36"
    Build Tools:
      - 34.0.0
      - 35.0.0
      - 36.0.0
    System Images:
      - android-35 | Google APIs ARM 64 v8a
      - android-35 | Google Play ARM 64 v8a
      - android-35 | Pre-Release 16 KB Page Size Google Play ARM 64 v8a
      - android-36 | Google APIs ARM 64 v8a
      - android-36 | Google Play ARM 64 v8a
      - android-36 | Pre-Release 16 KB Page Size Google Play ARM 64 v8a
    Android NDK: Not Found
IDEs:
  Android Studio: 2025.1 AI-251.26094.121.2512.13930704
  Xcode:
    version: 26.1.1/17B100
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 17.0.16
    path: /usr/bin/javac
  Ruby:
    version: 3.2.0
    path: /Users/cb/.rbenv/shims/ruby
npmPackages:
  "@react-native-community/cli": Not Found
  react: Not Found
  react-native: Not Found
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: false
  newArchEnabled: false
iOS:
  hermesEnabled: true
  newArchEnabled: true

Stacktrace or Logs

If I capture view hierarchy in Xcode, at first the horizontal ScrollView has this state:

<RCTEnhancedScrollView: 0x10cdc0e00; baseClass = UIScrollView; frame = (0 0; 382 661.333); clipsToBounds = YES; 
autoresize = W+H; gestureRecognizers = <NSArray: 0x600000d95e90>; layer = <CALayer: 0x600000d96220>; 
contentOffset: {0, 0}; contentSize: {200, 20}; adjustedContentInset: {0, 0, 0, 0} topEdge=<style=automatic backgroundCapture=color>>

Note the inset and offset: contentOffset: {0, 0}; contentSize: {200, 20}; adjustedContentInset: {0, 0, 0, 0}

If I navigate to the taller vertical ScrollView (and capture), it has this state:

<RCTEnhancedScrollView: 0x10cdc0e00; baseClass = UIScrollView; frame = (0 0; 382 661.333); clipsToBounds = YES; 
autoresize = W+H; gestureRecognizers = <NSArray: 0x600000d95e90>; layer = <CALayer: 0x600000d96220>; 
contentOffset: {0, 0}; contentSize: {382, 300}; adjustedContentInset: {0, 0, 0, 0} topEdge=<style=automatic backgroundCapture=color>>

Here the offset and inset is actually wrong, since it is supposed to center the content in the screen: contentOffset: {0, 0}; contentSize: {382, 300}; adjustedContentInset: {0, 0, 0, 0}

It has "inherited" those from the horizontal ScrollView. If I navigate back to the horizontal ScrollView:

<RCTEnhancedScrollView: 0x10cdc0e00; baseClass = UIScrollView; frame = (0 0; 382 661.333); clipsToBounds = YES; 
autoresize = W+H; gestureRecognizers = <NSArray: 0x600000d95e90>; layer = <CALayer: 0x600000d96220>; 
contentOffset: {0, -180.66666666666666}; contentSize: {200, 20}; adjustedContentInset: {180.66666666666663, 0, 180.66666666666663, 0} topEdge=<style=automatic backgroundCapture=color>>

Now these are wrong: contentOffset: {0, -180.66666666666666}; contentSize: {200, 20}; adjustedContentInset: {180.66666666666663, 0, 180.66666666666663, 0}

It has actually inherited the "center content" offset/inset from the vertical ScrollView.

On next navigate back to vertical, the content is properly centered for that ScrollView, but wrong going forward for the horizontal one.

Notice the pointer 0x10cdc0e00 is the same for all instances, so it actually recycled the same native ScrollView.

MANDATORY Reproducer

https://github.com/cbrevik/react-native/blob/0cd46dcec7521f4697c47b1a264b065cd4ca8c59/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js

Screenshots and Videos

Bugged Recycled ScrollView

bugged_recycled_scrollview.mov

Fixed Recycled ScrollView (with hacky patch)

fixed-recycled-scrollview.mov

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