You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Reproduced on 0.86.2 (latest stable) and on 0.81.4; the responsible code is linked at both versions below.
Mechanism
On the New Architecture, RCTScrollViewComponentView registers its UIKeyboardWillChangeFrameNotification observer once and keeps it for the native view's whole lifetime, gated only by the _automaticallyAdjustKeyboardInsets ivar:
observer registered: L163-L170. The comment there notes explicit unregistration is unnecessary because removal is automatic. That automatic removal happens at dealloc, a point a recycled view never reaches.
the ivar is assigned only inside an old-vs-new props diff in updateProps: L347-L349
prepareForRecycle (L616-L637) resets contentOffset and even resets the adjacent _shouldUpdateContentInsetAdjustmentBehavior flag (L629), but neither contentInset nor _automaticallyAdjustKeyboardInsets. On current main, contentInset and contentOffset are both reset in prepareForRecycle; the ivar and the observer still are not.
Consequence: when a ScrollView with automaticallyAdjustKeyboardInsets is unmounted while keyboard activity is in flight (for example a modal that auto-dismisses after a form submission while an autofocused input's keyboard is still animating), its recycled native view keeps armed keyboard machinery. A subsequently mounted plain ScrollView that receives this view from the pool gets keyboard insets applied to it, although it never enabled the prop and contains no inputs. On 0.81.4 the un-reset contentInset can additionally carry over directly; on newer versions the values are cleaned but the surviving handler re-applies keyboard geometry to whatever the view has become.
Expected
A freshly mounted ScrollView with no inputs and without automaticallyAdjustKeyboardInsets reports contentInset.bottom = 0 with the keyboard hidden and never reacts to keyboard events.
Actual (iPhone, iOS 26.5.2, New Architecture, release builds; reproduced on RN 0.86.2 and 0.81.4)
The victim ScrollView reports a phantom contentInset.bottom = 217.3 (its geometric overlap with the keyboard) with the keyboard hidden, on every poisoned cycle.
Scrolling parks settled at maxOffset + 217 in blank space with no bounce-back, because the phantom inset legalizes the region.
Live-listener test: with the pool contaminated, focusing an unrelated TextInput outside the victim and toggling the keyboard moves the victim's contentOffset by itself. Nothing touches the victim after mount. This matches the handler's tail, which animates contentInset, verticalScrollIndicatorInsets, and a scrollTo: in one block, so an armed recycled view both re-stamps insets and has its offset shoved by other inputs' keyboards.
The value never grows across further keyboard events, consistent with the handler assigning the geometric overlap absolutely rather than accumulating.
Contamination persists across further cycles and config changes until the process dies.
Controls, each from a fresh process: with automaticallyAdjustKeyboardInsets off, 0 findings across identical cycles; with the keyboard dismissed before the modal closes, 0 findings. Both ingredients are required.
On 0.86.2 the same harness files reproduce unchanged, with the same phantom inset value; in our run a rogue keyboard event (keyboard activity while the inputless victim was mounted) fired on every cycle.
The only notification the observer subscribes to is UIKeyboardWillChangeFrameNotification, so the re-stamp necessarily comes from those deliveries; our JS-side log shows the keyboard-hide event of each cycle arriving after the modal's teardown. We did not instrument the native side per delivery.
Impact
We hit this in a production app: a logging modal auto-dismissed on success while the keyboard was animating; from then on every freshly mounted screen or modal opened normally but ran into un-escapable blank space on first scroll, until app restart. Full-screen victims inherit the full keyboard height, and under normal keyboard traffic the live handler keeps shoving the offset. Removing automaticallyAdjustKeyboardInsets app-wide eliminated it, validated by deliberately re-arming the trigger.
Suggested direction
Reset _automaticallyAdjustKeyboardInsets in prepareForRecycle (or remove/re-add the observer around recycling), and consider assigning the ivar unconditionally in updateProps instead of inside the diff, so a recycled view can never retain another component's opt-in.
Related
#55090 (scroll state surviving recycling; #56832 fixed the centerContent path), #56189 (stale keyboard inset after dismissal on the same view, open), #47731, #41397, #34165. Distinct from the stale-value family: the surviving element here is behavior, which re-creates the values even where they are reset.
Run on a physical device, default config (automaticallyAdjustKeyboardInsets on, close after 0ms).
Tap Auto x10. Each cycle opens a Modal whose aAKI ScrollView autofocuses a TextInput, closes it mid-keyboard without dismissing, then freshly mounts a plain instrumented ScrollView ("victim").
Watch the counters and the red log lines: the victim reports the phantom contentInset via its scroll events, so no native inspection is needed to observe the defect.
On the iOS Simulator the harness runs and shows the same keyboard-hide-after-teardown pattern in its event log, but we did not observe corruption in brief simulator runs; the primary evidence is from physical devices.
The reproducer is a bare @react-native-community/cli init app with no dependencies beyond react-native. The harness files are version-portable: they reproduce unchanged on both 0.81.4 and 0.86.2 (see the README results table). A Snack is unsuitable because the race requires physical-device keyboard timing and process-restart control. Happy to port onto the reproducer-react-native template or RNTesterPlayground if that helps triage.
React Native Version
RN 0.86.2
Affected Platforms
Runtime - iOS
Output of npx @react-native-community/cli info
System:
OS: macOS 26.5.2
CPU: (10) arm64 Apple M2 Pro
Memory: 118.98 MB / 16.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 20.19.5
path: /Users/pragathijayaram/.nvm/versions/node/v20.19.5/bin/node
Yarn: Not Found
npm:
version: 10.8.2
path: /Users/pragathijayaram/.nvm/versions/node/v20.19.5/bin/npm
Watchman: Not Found
Managers:
CocoaPods:
version: 1.16.2
path: /opt/homebrew/bin/pod
SDKs:
iOS SDK:
Platforms:
- DriverKit 25.2
- iOS 26.2
- macOS 26.2
- tvOS 26.2
- visionOS 26.2
- watchOS 26.2
Android SDK: Not Found
IDEs:
Android Studio: 2026.1 AI-261.23567.138.2611.15646644
Xcode:
version: 26.3/17C529
path: /usr/bin/xcodebuild
Languages:
Java: Not Found
Ruby:
version: 2.6.10
path: /usr/bin/ruby
npmPackages:
"@react-native-community/cli":
installed: 20.1.0
wanted: 20.1.0
react:
installed: 19.2.3
wanted: 19.2.3
react-native:
installed: 0.86.2
wanted: 0.86.2
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: true
newArchEnabled: true
iOS:
hermesEnabled: true
newArchEnabled: true
Stacktrace or Logs
There is no crash; the failure is behavioral. Below are excerpts from the reproducer's on-screen event log (Release build, physical device), which mirrors everything to the harness UI. The CORRUPT lines are emitted by the victim ScrollView's own scroll events reporting its native `contentInset`/`contentOffset`.
Poisoning cycle pattern (each cycle; the keyboard-hide of the poison modal arrives after the modal's teardown, while the next screen is already mounting):
14:25:14.750 keyboardWillShow
14:25:15.245 keyboardWillHide (phase victim)
14:10:38.135 keyboardWillHide (phase gap)
keyboardWillShow during gap - ROGUE <- keyboard event with no input focused anywhere
Corrupt readings on the freshly mounted victim (RN 0.86.2 run; keyboard hidden, victim has no inputs and never enabled automaticallyAdjustKeyboardInsets; content 840pt, viewport 234pt, so the honest max offset is 606 and the phantom inset legalizes parking at ~823):
15:37:21.469 CORRUPT[inline]: insetB=217.3 insetT=0.0 y=810.0
15:37:21.485 CORRUPT[inline]: insetB=217.3 insetT=0.0 y=810.3
15:37:21.523 CORRUPT[inline]: insetB=217.3 insetT=0.0 y=809.3
15:37:21.548 CORRUPT[inline]: insetB=217.3 insetT=0.0 y=809.7
15:37:21.569 CORRUPT[inline]: insetB=217.3 insetT=0.0 y=810.0
15:37:21.598 CORRUPT[inline]: insetB=217.3 insetT=0.0 y=810.3
15:37:21.620 CORRUPT[inline]: insetB=217.3 insetT=0.0 y=810.3
Counters after 3 cycles on 0.86.2: rogue keyboard 3, corrupt readings 203, max insetB 217.3
Line formats and emission points are in the reproducer's `App.tsx` (`pushLog`, `onReadingImpl`).
Description
Reproduced on 0.86.2 (latest stable) and on 0.81.4; the responsible code is linked at both versions below.
Mechanism
On the New Architecture,
RCTScrollViewComponentViewregisters itsUIKeyboardWillChangeFrameNotificationobserver once and keeps it for the native view's whole lifetime, gated only by the_automaticallyAdjustKeyboardInsetsivar:updateProps: L347-L349prepareForRecycle(L616-L637) resetscontentOffsetand even resets the adjacent_shouldUpdateContentInsetAdjustmentBehaviorflag (L629), but neithercontentInsetnor_automaticallyAdjustKeyboardInsets. On currentmain,contentInsetandcontentOffsetare both reset inprepareForRecycle; the ivar and the observer still are not.Consequence: when a ScrollView with
automaticallyAdjustKeyboardInsetsis unmounted while keyboard activity is in flight (for example a modal that auto-dismisses after a form submission while an autofocused input's keyboard is still animating), its recycled native view keeps armed keyboard machinery. A subsequently mounted plain ScrollView that receives this view from the pool gets keyboard insets applied to it, although it never enabled the prop and contains no inputs. On 0.81.4 the un-resetcontentInsetcan additionally carry over directly; on newer versions the values are cleaned but the surviving handler re-applies keyboard geometry to whatever the view has become.Expected
A freshly mounted ScrollView with no inputs and without
automaticallyAdjustKeyboardInsetsreportscontentInset.bottom = 0with the keyboard hidden and never reacts to keyboard events.Actual (iPhone, iOS 26.5.2, New Architecture, release builds; reproduced on RN 0.86.2 and 0.81.4)
contentInset.bottom = 217.3(its geometric overlap with the keyboard) with the keyboard hidden, on every poisoned cycle.maxOffset + 217in blank space with no bounce-back, because the phantom inset legalizes the region.contentOffsetby itself. Nothing touches the victim after mount. This matches the handler's tail, which animatescontentInset,verticalScrollIndicatorInsets, and ascrollTo:in one block, so an armed recycled view both re-stamps insets and has its offset shoved by other inputs' keyboards.automaticallyAdjustKeyboardInsetsoff, 0 findings across identical cycles; with the keyboard dismissed before the modal closes, 0 findings. Both ingredients are required.UIKeyboardWillChangeFrameNotification, so the re-stamp necessarily comes from those deliveries; our JS-side log shows the keyboard-hide event of each cycle arriving after the modal's teardown. We did not instrument the native side per delivery.Impact
We hit this in a production app: a logging modal auto-dismissed on success while the keyboard was animating; from then on every freshly mounted screen or modal opened normally but ran into un-escapable blank space on first scroll, until app restart. Full-screen victims inherit the full keyboard height, and under normal keyboard traffic the live handler keeps shoving the offset. Removing
automaticallyAdjustKeyboardInsetsapp-wide eliminated it, validated by deliberately re-arming the trigger.Suggested direction
Reset
_automaticallyAdjustKeyboardInsetsinprepareForRecycle(or remove/re-add the observer around recycling), and consider assigning the ivar unconditionally inupdatePropsinstead of inside the diff, so a recycled view can never retain another component's opt-in.Related
#55090 (scroll state surviving recycling; #56832 fixed the
centerContentpath), #56189 (stale keyboard inset after dismissal on the same view, open), #47731, #41397, #34165. Distinct from the stale-value family: the surviving element here is behavior, which re-creates the values even where they are reset.Steps to reproduce
Minimal reproducer with an automated harness: https://github.com/PragathiJ/rn-aaki-recycle-repro
automaticallyAdjustKeyboardInsetson, close after 0ms).contentInsetvia its scroll events, so no native inspection is needed to observe the defect.On the iOS Simulator the harness runs and shows the same keyboard-hide-after-teardown pattern in its event log, but we did not observe corruption in brief simulator runs; the primary evidence is from physical devices.
The reproducer is a bare
@react-native-community/cli initapp with no dependencies beyond react-native. The harness files are version-portable: they reproduce unchanged on both 0.81.4 and 0.86.2 (see the README results table). A Snack is unsuitable because the race requires physical-device keyboard timing and process-restart control. Happy to port onto the reproducer-react-native template or RNTesterPlayground if that helps triage.React Native Version
RN 0.86.2
Affected Platforms
Runtime - iOS
Output of
npx @react-native-community/cli infoStacktrace or Logs
MANDATORY Reproducer
https://github.com/PragathiJ/rn-aaki-recycle-repro
Screenshots and Videos
aaki-recycle-repro.mp4