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
{{ message }}
Repository navigation
[$175] [Sentry: APP-2AP] RNSNavigationController pushes same screen instance twice #102743
Error: NSInvalidArgumentException: RNSNavigationController is pushing the same view controller instance (RNSScreen) more than once
Suspected cause
react-native-screens pushes the same screen instance twice, likely from a duplicate navigation action firing before the first push animation completes.
Reproduction
Unknown — see Sentry events linked from the Sentry issue.
changed the title [-][Sentry: APP-2AP] RNSNavigationController pushes same screen instance twice[/-][+][$175] [Sentry: APP-2AP] RNSNavigationController pushes same screen instance twice[/+]on Oct 1, 2026
⚠️Confidence: medium. The native code path is confirmed. The exact user action that triggers it is not, because I couldn't load the Sentry stack trace for APP-2AP. No repro is known.
What is the root cause of that problem?
This is a bug in react-native-screens. It happens when two stack updates arrive before UIKit has run a push.
setPushViewControllers has the only push call in the stack: RNSScreenStack.mm:657-665. It decides whether to push by checking two things:
_controller.viewControllers does not already contain the screen.
UIKit can accept a push and run it after the next Core Animation commit. Until then, the screen is not in viewControllers and transitionCoordinator is nil, so both checks pass. A second updateContainer then calls pushViewController or setViewControllers again with the same RNSScreen. When UIKit runs the deferred push, it throws NSInvalidArgumentException.
Several things can call updateContainer back to back:
one dispatch_async per Fabric mounting transaction
didMoveToWindow when a bottom tab is attached
markChildUpdated
the transition completion block
In the App, any of these can cause two updates in a row:
Upstream has not fixed this. 4.28.0 is the latest 4.x release, and upstream main has the same push logic with no guard against a pending push. The older upstream report RNScreensNavigationController is pushing the same view controller instance more than once was closed in 2021. Users kept reporting the same crash there through 4.4.0, with the same _UIAfterCACommitBlock → pushViewController:transition:forceImmediate: stack.
The App has no patch for react-native-screens, so a local patch did not cause this.
What changes do you think we should make in order to solve the problem?
Patch react-native-screens with patch-package, and send the same fix upstream:
In RNSScreenStackView, remember the screen being pushed when pushViewController:top is called at :665. Clear it in the existing navigationController:didShowViewController: delegate method (:988).
At the start of setPushViewControllers, if a push is still pending, don't change the stack. Schedule one updateContainer for after the push finishes, using the same _updateScheduled pattern the method already uses for transitionCoordinator.
Before calling pushViewController, also skip the push if top.parentViewController == _controller.
Document the patch in patches/react-native-screens/details.md, and link the upstream issue or PR.
The next update then applies the final state once, so the user ends up on the correct screen and the app doesn't crash.
What alternative solutions did you explore? (Optional)
Debounce navigation in JS (Navigation.ts or linkTo). I rejected this. Many native paths call updateContainer without a JS navigation call (tab attach, mount transactions), so it would not cover every trigger. It would also slow down navigation everywhere.
Look at usePreventRemove in ScreenWrapper (ScreenWrapper/index.tsx:204-210). This turns on RNS's preventNativeDismiss flow, which pushes the same RNSScreen again on purpose after a native dismiss is blocked. It is a possible second trigger. The native guard above covers it too.
What would raise confidence
Check the Sentry APP-2AP events for:
The frames below -[RNSScreenStackView setPushViewControllers:]. They show which caller made the second update: the mount dispatch_async, didMoveToWindow, viewDidDisappear (preventNativeDismiss), or the transition completion.
_UIAfterCACommitBlock / _runAfterCACommitDeferredBlocks in the stack. Seeing them confirms the deferred-push mechanism.
Breadcrumbs showing two navigation actions within about one frame, for example after a notification tap, a deep link, or a return from OldDot.
Next Steps for Contributor+ team: To accept:@MelvinBot implement [this](https://github.com/Expensify/App/issues/102743) to create a draft PR. To refine:@MelvinBot <your feedback> To reject: Explain why you are rejecting Melvin's proposal.
Please re-state the problem that we are trying to solve in this issue.
On iOS (HybridApp), the app crashes with an NSInvalidArgumentException stating RNSNavigationController is pushing the same view controller instance (RNSScreen) more than once when rapid screen pushes or state updates occur before UIKit completes the initial push animation.
What is the root cause of that problem?
In react-native-screens (RNSScreenStack.mm:657–665), setPushViewControllers checks if a screen is already in _controller.viewControllers and whether transitionCoordinator is nil.
When rapid navigation dispatches (e.g. double taps, deep links, tab switches) happen before the Core Animation commit finishes, the screen is not yet present in viewControllers, passing both checks and causing pushViewController to be called a second time with the exact same RNSScreen instance.
What changes do you think we should make in order to solve the problem?
Apply a patch to react-native-screens using patch-package (and add documentation to patches/react-native-screens/details.md):
Track the pending screen instance during pushViewController:top in RNSScreenStackView.
Clear the pending push tracker inside the delegate callback navigationController:didShowViewController:.
At the beginning of setPushViewControllers, if a push operation is currently pending or top.parentViewController == _controller, skip the immediate push and queue a single _updateScheduled run for after the transition completes.
What alternative solutions did you consider?
Debouncing navigation actions in JS (src/libs/Navigation/Navigation.ts), but this was rejected because it doesn't prevent native mounting transactions or tab attachment dispatches from triggering duplicate pushes on the native side.
I'm still investigating it 🕵️ I'm checking if it's possible to create an application that reproduces the issue. If so, I'll report it and try to fix it upstream
I created a draft PR: #103603. I discussed this issue with the react-native-screens maintainers and we agreed that a patch is a good solution for now. The bug should be fixed properly in RNS 5.0, at which point we can remove the patch 🫡
Activity
melvin-bot commentedon Oct 1, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @mkhutornyi (
External)[-][Sentry: APP-2AP] RNSNavigationController pushes same screen instance twice[/-][+][$175] [Sentry: APP-2AP] RNSNavigationController pushes same screen instance twice[/+]melvin-bot commentedon Oct 1, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022105599577912259397
MelvinBot commentedon Oct 1, 2026
Proposal
What is the root cause of that problem?
This is a bug in
react-native-screens. It happens when two stack updates arrive before UIKit has run a push.setPushViewControllershas the only push call in the stack:RNSScreenStack.mm:657-665. It decides whether to push by checking two things:_controller.viewControllersdoes not already contain the screen.transitionCoordinatoris nil (:597-611).UIKit can accept a push and run it after the next Core Animation commit. Until then, the screen is not in
viewControllersandtransitionCoordinatoris nil, so both checks pass. A secondupdateContainerthen callspushViewControllerorsetViewControllersagain with the sameRNSScreen. When UIKit runs the deferred push, it throwsNSInvalidArgumentException.Several things can call
updateContainerback to back:dispatch_asyncper Fabric mounting transactiondidMoveToWindowwhen a bottom tab is attachedmarkChildUpdatedIn the App, any of these can cause two updates in a row:
linkTodispatching two actionsEvery App navigator except the tab navigators renders
NativeStackViewon iOS (createPlatformStackNavigatorComponent/index.native.tsx:99).Upstream has not fixed this.
4.28.0is the latest 4.x release, and upstreammainhas the same push logic with no guard against a pending push. The older upstream report RNScreensNavigationController is pushing the same view controller instance more than once was closed in 2021. Users kept reporting the same crash there through 4.4.0, with the same_UIAfterCACommitBlock→pushViewController:transition:forceImmediate:stack.The App has no patch for
react-native-screens, so a local patch did not cause this.What changes do you think we should make in order to solve the problem?
Patch
react-native-screenswithpatch-package, and send the same fix upstream:RNSScreenStackView, remember the screen being pushed whenpushViewController:topis called at:665. Clear it in the existingnavigationController:didShowViewController:delegate method (:988).setPushViewControllers, if a push is still pending, don't change the stack. Schedule oneupdateContainerfor after the push finishes, using the same_updateScheduledpattern the method already uses fortransitionCoordinator.pushViewController, also skip the push iftop.parentViewController == _controller.patches/react-native-screens/details.md, and link the upstream issue or PR.The next update then applies the final state once, so the user ends up on the correct screen and the app doesn't crash.
What alternative solutions did you explore? (Optional)
Navigation.tsorlinkTo). I rejected this. Many native paths callupdateContainerwithout a JS navigation call (tab attach, mount transactions), so it would not cover every trigger. It would also slow down navigation everywhere.usePreventRemoveinScreenWrapper(ScreenWrapper/index.tsx:204-210). This turns on RNS'spreventNativeDismissflow, which pushes the sameRNSScreenagain on purpose after a native dismiss is blocked. It is a possible second trigger. The native guard above covers it too.What would raise confidence
Check the Sentry APP-2AP events for:
-[RNSScreenStackView setPushViewControllers:]. They show which caller made the second update: the mountdispatch_async,didMoveToWindow,viewDidDisappear(preventNativeDismiss), or the transition completion._UIAfterCACommitBlock/_runAfterCACommitDeferredBlocksin the stack. Seeing them confirms the deferred-push mechanism.Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/102743)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
view run
mkhutornyi commentedon Oct 1, 2026
@mountiny this looks like a good candidate for SWM navigation team?
jamarnat12 commentedon Oct 1, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
On iOS (HybridApp), the app crashes with an
NSInvalidArgumentExceptionstatingRNSNavigationController is pushing the same view controller instance (RNSScreen) more than oncewhen rapid screen pushes or state updates occur before UIKit completes the initial push animation.What is the root cause of that problem?
In
react-native-screens(RNSScreenStack.mm:657–665),setPushViewControllerschecks if a screen is already in_controller.viewControllersand whethertransitionCoordinatorisnil.When rapid navigation dispatches (e.g. double taps, deep links, tab switches) happen before the Core Animation commit finishes, the screen is not yet present in
viewControllers, passing both checks and causingpushViewControllerto be called a second time with the exact sameRNSScreeninstance.What changes do you think we should make in order to solve the problem?
Apply a patch to
react-native-screensusingpatch-package(and add documentation topatches/react-native-screens/details.md):pushViewController:topinRNSScreenStackView.navigationController:didShowViewController:.setPushViewControllers, if a push operation is currently pending ortop.parentViewController == _controller, skip the immediate push and queue a single_updateScheduledrun for after the transition completes.What alternative solutions did you consider?
Debouncing navigation actions in JS (
src/libs/Navigation/Navigation.ts), but this was rejected because it doesn't prevent native mounting transactions or tab attachment dispatches from triggering duplicate pushes on the native side.Contributor details
Your Expensify account email: jacobtrevino444@gmail.com
Upwork Profile Link: https://www.upwork.com/freelancers/~01cc432d2b667fd0ff
melvin-bot commentedon Oct 1, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
WojtekBoman commentedon Oct 2, 2026
Hey, I'll take a look at it!
WojtekBoman commentedon Oct 5, 2026
I'm still investigating it 🕵️ I'm checking if it's possible to create an application that reproduces the issue. If so, I'll report it and try to fix it upstream
WojtekBoman commentedon Oct 6, 2026
I built an app that reproduces this issue https://github.com/WojtekBoman/rns-ios17-push-same-vc-twice-repro. I sent it to the
react-native-screensteam and am waiting for their feedback on whether this should be fixed directly in the library 🫡WojtekBoman commentedon Oct 9, 2026
I created a draft PR: #103603. I discussed this issue with the
react-native-screensmaintainers and we agreed that a patch is a good solution for now. The bug should be fixed properly in RNS 5.0, at which point we can remove the patch 🫡It should be ready for review early next week!
mountiny commentedon Oct 12, 2026
Thank you for looking into the PR.