Skip to content

iOS Release crash on tab select: nil selectedScreenKey reaches -[RNSTabsNavigationState cloneState] (stringWithString:nil) #4141

@genchats

Description

@genchats

Description

In Release builds on iOS 26, selecting a bottom tab can crash the app with NSInvalidArgumentException inside -[RNSTabsNavigationState cloneState]:

2   Foundation                     -[NSPlaceholderString initWithString:] + 940 (NSString.m:2327)
3   Foundation                     +[NSString stringWithString:] + 32 (NSString.m:213)
4   app                            -[RNSTabsNavigationState cloneState] + 76 (RNSTabsNavigationState.mm:17)
5   app                            -[RNSTabBarController progressNavigationState:withOrigin:] + 124 (RNSTabBarController.mm:648)
6   app                            -[RNSTabBarController updateNavigationStateOnModelUpdate] + 48 (RNSTabBarController.mm:315)
7   app                            -[RNSTabBarController userDidSelectViewController:] + 96 (RNSTabBarController.mm:355)
8   app                            -[RNSTabBarController tabBarController:didSelectViewController:] + 24 (RNSTabBarController.mm:448)
9   UIKitCore                      -[UITabBarController _setSelectedViewControllerAndNotify:] + 276

Root cause

On tab selection, updateNavigationStateOnModelUpdate derives the key via screenKeyForSelectedViewControllerscreenKeyForViewController:getScreenKeyOrNull, which reads self.tabScreenComponentView.screenKey (RNSTabsScreenViewController.mm). That is nil while the selected tab's component view is detached — e.g. while tab triggers are being remounted. In our app, a persisted "show this tab" toggle rehydrates from storage shortly after cold launch and remounts the <NativeTabs.Trigger> list (expo-router); a tab tap landing in that window reliably produces the nil.

Every guard on this path is RCTAssert (progressNavigationState: line ~637, screenKeyForViewController: line ~672), which compiles out of Release builds. So the nil key is silently stored into _navigationState, and the app aborts on the next cloneState call at RNSTabsNavigationState.mm:17 ([NSString stringWithString:nil] throws). Debug builds would have tripped the assert instead, which is presumably why this survived testing.

Still present on main (ios/tabs/host/RNSTabsNavigationState.mm / RNSTabBarController.mm).

Suggested fix

  1. In progressNavigationState:withOrigin:, early-return (or keep current state) when newSelectedScreenKey == nil — a Release-mode mirror of the existing assert; UIKit state is reconciled on the next container update.
  2. In cloneState / cloneRequest, use [self.selectedScreenKey copy] instead of [NSString stringWithString:...] — equivalent for strings, nil-safe.

We're shipping exactly this as a patch-package patch and it resolves the crash. Happy to open a PR if useful.

Steps to reproduce

  1. iOS app using native bottom tabs (we hit it via expo-router unstable-native-tabs) with tab triggers that remount shortly after launch (e.g. a trigger's hidden prop flips after async state rehydration)
  2. Release build (so RCTAssert is compiled out), iOS 26 device
  3. Cold launch, tap a bottom tab within the first seconds while triggers remount

Snack or a link to a repository

Crash observed in a production TestFlight app (full Apple crash log available on request — incident BA77F785-5D9D-4651-9FB0-BEFE15EFB0B6).

Screens version

4.25.2

React Native version

0.83.4

Platforms

iOS

Device

Real device

Device model

iPhone 15 Pro (iPhone16,1), iOS 26.5

Architecture

Fabric (New Architecture)

Metadata

Metadata

Assignees

No one assigned

    Labels

    missing-reproThis issue need minimum repro scenarioplatform:iosIssue related to iOS part of the library

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions