Skip to content

[$175] [Sentry: APP-2AP] RNSNavigationController pushes same screen instance twice #102743

Description

@mountiny

Sentry

https://expensify.sentry.io/issues/APP-2AP

Impact (snapshot at filing)

  • Users (total since first seen): 36
  • Events: 174
  • Users (last 14d): not pulled separately
  • First seen: 2025-12-09 (~10 months ago, still ongoing)
  • Last seen: now (ongoing)
  • Platform: iOS (HybridApp)
  • App version(s): seen across 9.4.93 through current production
  • Affected route(s) (parameterized): n/a — native navigation push, not a JS route
  • Mechanism: nsexception (fatal)

Stack trace (top frames, first-party only)

-[UINavigationController pushViewController:transition:forceImmediate:]
objc_exception_throw
__exceptionPreprocess

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.

Related

  • Sibling Sentry IDs: none identified
  • Prior GH issues: none found
Upwork Automation - Do Not Edit
Issue OwnerCurrent Issue Owner: @WojtekBoman

Activity

  1. self-assigned this
    on Oct 1, 2026
  2. added
    ExternalAdded to denote the issue can be worked on by a contributor
    BugSomething is broken. Auto assigns a BugZero manager.
    on Oct 1, 2026
  3. melvin-bot commented on Oct 1, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @mkhutornyi (External)

  4. 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
  5. melvin-bot commented on Oct 1, 2026

    @melvin-bot
  6. MelvinBot commented on Oct 1, 2026

    @MelvinBot
    Contributor

    Proposal

    ⚠️ 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.
    • transitionCoordinator is nil (:597-611).

    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:

    • linkTo dispatching two actions
    • a tab switch combined with a push
    • a notification tap or deep link

    Every App navigator except the tab navigators renders NativeStackView on iOS (createPlatformStackNavigatorComponent/index.native.tsx:99).

    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:

    1. In RNSScreenStackView, remember the screen being pushed when pushViewController:top is called at :665. Clear it in the existing navigationController:didShowViewController: delegate method (:988).
    2. 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.
    3. Before calling pushViewController, also skip the push if top.parentViewController == _controller.
    4. 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.


    view run

  7. mkhutornyi commented on Oct 1, 2026

    @mkhutornyi
    Contributor

    @mountiny this looks like a good candidate for SWM navigation team?

  8. jamarnat12 commented on Oct 1, 2026

    @jamarnat12

    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):

    1. Track the pending screen instance during pushViewController:top in RNSScreenStackView.
    2. Clear the pending push tracker inside the delegate callback navigationController:didShowViewController:.
    3. 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.

    Contributor details

    Your Expensify account email: jacobtrevino444@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/~01cc432d2b667fd0ff

  9. melvin-bot commented on Oct 1, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  10. WojtekBoman commented on Oct 2, 2026

    @WojtekBoman
    Contributor

    Hey, I'll take a look at it!

  11. WojtekBoman commented on Oct 5, 2026

    @WojtekBoman
    Contributor

    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

  12. WojtekBoman commented on Oct 6, 2026

    @WojtekBoman
    Contributor

    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-screens team and am waiting for their feedback on whether this should be fixed directly in the library 🫡

  13. WojtekBoman commented on Oct 9, 2026

    @WojtekBoman
    Contributor

    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 🫡

    It should be ready for review early next week!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions