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 screenKeyForSelectedViewController → screenKeyForViewController: → 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
- 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.
- 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
- 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)
- Release build (so
RCTAssert is compiled out), iOS 26 device
- 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)
Description
In Release builds on iOS 26, selecting a bottom tab can crash the app with
NSInvalidArgumentExceptioninside-[RNSTabsNavigationState cloneState]:Root cause
On tab selection,
updateNavigationStateOnModelUpdatederives the key viascreenKeyForSelectedViewController→screenKeyForViewController:→getScreenKeyOrNull, which readsself.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 nextcloneStatecall atRNSTabsNavigationState.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
progressNavigationState:withOrigin:, early-return (or keep current state) whennewSelectedScreenKey == nil— a Release-mode mirror of the existing assert; UIKit state is reconciled on the next container update.cloneState/cloneRequest, use[self.selectedScreenKey copy]instead of[NSString stringWithString:...]— equivalent for strings, nil-safe.We're shipping exactly this as a
patch-packagepatch and it resolves the crash. Happy to open a PR if useful.Steps to reproduce
unstable-native-tabs) with tab triggers that remount shortly after launch (e.g. a trigger'shiddenprop flips after async state rehydration)RCTAssertis compiled out), iOS 26 deviceSnack 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)