Skip to content

[$250] [Sentry: APP-N5H] HybridApp Android: native tab bar crashes with more than 6 items #104505

Open

Description

@mountiny

Sentry

https://expensify.sentry.io/issues/APP-N5H

Impact (snapshot at filing)

  • Users (total since first seen): 39
  • Events: 143
  • Users (last 14d, all versions): 39
  • Users (last 14d, production 9.5.11 to 9.5.13): 1
  • First seen: 2026-10-16 (4 days ago)
  • Last seen: 2026-10-20 (ongoing, Sentry marks it as escalating)
  • Platform: Android HybridApp
  • App version(s): 9.5.13-0 (31 users) and 9.5.13-1 (4 users), plus 1 user each on 9.5.9-4, 9.5.10-0, 9.5.11-0, 9.5.13-3 and 9.5.14-0
  • Affected route(s) (parameterized): native screen; the sample event was on Settings_Troubleshoot
  • Mechanism: UncaughtExceptionHandler, level fatal

Most users were on 9.5.13-0, a staging build. 9.5.13-6 is now rolling out to production on Android, so this may grow.

Stack trace (top frames, first-party only)

com.swmansion.rnscreens.ScreenStack.onUpdate (ScreenStack.kt:323)
com.swmansion.rnscreens.gamma.tabs.container.TabsContainer.onAttachedToWindow (TabsContainer.kt:311)
com.swmansion.rnscreens.gamma.tabs.container.TabsContainer.flushPendingUpdates (TabsContainer.kt:212)
com.swmansion.rnscreens.gamma.tabs.container.TabsContainer.updateNavigationMenuStructure (TabsContainer.kt:545)
com.swmansion.rnscreens.gamma.tabs.container.MenuHelpersKt.getOrCreateMenuItemForFragment (MenuHelpers.kt:8)
androidx.appcompat.view.menu.MenuBuilder.add
IllegalArgumentException: Maximum number of items supported by CustomBottomNavigationView is 6

Suspected cause

The native tab container adds a seventh item to the Android bottom navigation menu, which supports at most 6. It happens when a screen stack attaches the tab container again, so the tab menu items may be added again instead of reused.

Reproduction

Unknown. See the Sentry events linked from the Sentry issue.

Related

Upwork Automation - Do Not Edit
Issue OwnerCurrent Issue Owner: @wildan-m

Activity

  1. self-assigned this
    on Oct 20, 2026
  2. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Oct 20, 2026
  3. melvin-bot commented on Oct 20, 2026

    @melvin-bot

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

  4. changed the title [-][Sentry: APP-N5H] HybridApp Android: native tab bar crashes with more than 6 items[/-] [+][$175] [Sentry: APP-N5H] HybridApp Android: native tab bar crashes with more than 6 items[/+] on Oct 20, 2026
  5. melvin-bot commented on Oct 20, 2026

    @melvin-bot
  6. RajGahoi commented on Oct 20, 2026

    @RajGahoi

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    HybridApp on Android crashes with IllegalArgumentException: Maximum number of items supported by CustomBottomNavigationView is 6 when a screen stack re-attaches the native tab container or when more than 6 navigation items are added.

    What is the root cause of that problem?

    Android's BottomNavigationView / CustomBottomNavigationView enforces a hard limit of maximum 6 items (MAX_ITEM_COUNT = 6).

    In PR #101339 and patch @react-navigation+bottom-tabs+7.19.2+003+hidden-tab-items.patch, the app registers 6 tab routes:
    HOME, REPORTS_SPLIT_NAVIGATOR, SEARCH_FULLSCREEN_NAVIGATOR, INSIGHTS, WORKSPACE_NAVIGATOR, SETTINGS_SPLIT_NAVIGATOR.
    Although getTabWithoutBarItem designates one route as abBarItemHidden: true to stay at 5 items, when a screen stack re-attaches (ScreenStack.kt:onUpdate -> TabsContainer.kt:onAttachedToWindow -> flushPendingUpdates -> updateNavigationMenuStructure), menu items from the previous fragment attachment can persist or re-accumulate without clearing or recycling if lushPendingUpdates runs repeatedly during stack transitions. Adding the new items pushes the menu size to 7 (or above), throwing IllegalArgumentException: Maximum number of items supported by CustomBottomNavigationView is 6.

    What changes do you think we should make in order to solve the problem?

    1. In TabsContainer.kt (or via patch to
      eact-native-screens), before calling updateNavigationMenuStructure / getOrCreateMenuItemForFragment, ensure menu.clear() is called or check that existing menu items matching the fragment tabs are reused/updated rather than re-added.
    2. In @react-navigation+bottom-tabs+7.19.2+003+hidden-tab-items.patch, strictly guard that routes marked with abBarItemHidden: true are never passed as native tab fragments to RNScreens under any transition state.

    Automated Tests

    Add a test in ScreenStackTest / navigation tests that mounts the native tab navigator, performs stack pushes and pops (such as opening Settings_Troubleshoot), and verifies that the menu items count in BottomNavigationView never exceeds 5.

    Contributor details
    Your Expensify account email: [email protected]

  7. RajGahoi commented on Oct 20, 2026

    @RajGahoi

    Contributor details update:
    Upwork Profile Link: https://www.upwork.com/freelancers/~014898525b9c53fc32

  8. jloa-dev commented on Oct 21, 2026

    @jloa-dev

    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/~0163955a2ba221e513

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    On Android HybridApp, navigating between screens (e.g., visiting Settings_Troubleshoot or during screen transitions inside a ScreenStack) crashes fatally with:
    IllegalArgumentException: Maximum number of items supported by CustomBottomNavigationView is 6
    thrown from:

    com.swmansion.rnscreens.ScreenStack.onUpdate (ScreenStack.kt:323)
    com.swmansion.rnscreens.gamma.tabs.container.TabsContainer.onAttachedToWindow (TabsContainer.kt:311)
    com.swmansion.rnscreens.gamma.tabs.container.TabsContainer.flushPendingUpdates (TabsContainer.kt:212)
    com.swmansion.rnscreens.gamma.tabs.container.TabsContainer.updateNavigationMenuStructure (TabsContainer.kt:545)
    com.swmansion.rnscreens.gamma.tabs.container.MenuHelpersKt.getOrCreateMenuItemForFragment (MenuHelpers.kt:8)
    androidx.appcompat.view.menu.MenuBuilder.add
    

    What is the root cause of that problem?

    1. In react-native-screens (gamma tabs on Android), TabsContainer manages bottom navigation tabs using CustomBottomNavigationView (based on Android Material's BottomNavigationView). Android's bottom navigation menu enforces a strict hardware constraint:
      MAX_ITEM_COUNT = 6 (or 5 standard, 6 custom). Adding a 7th item to MenuBuilder throws IllegalArgumentException.
    2. In ScreenStack.kt:286, when screen fragments are updated or re-appended in stack order, the fragment containing the tab bar is detached and re-attached to the window (transaction.remove(wrapper.fragment) followed by transaction.add(...)).
    3. When TabsContainer.onAttachedToWindow() executes, it calls flushPendingUpdates() -> updateNavigationMenuStructure().
    4. In updateNavigationMenuStructure(), TabsContainer iterates through its tab fragments and calls getOrCreateMenuItemForFragment(fragment) which delegates to bottomNavigationView.menu.add(...).
    5. However, updateNavigationMenuStructure() does not clear or synchronize the existing bottomNavigationView.menu prior to re-populating items. Because items from the initial layout pass are already present in the menu, re-adding the tabs appends duplicate menu items to the MenuBuilder. As soon as menu.size() reaches 7 (e.g. 4 tabs added twice = 8 items), Android throws IllegalArgumentException: Maximum number of items supported by CustomBottomNavigationView is 6.

    What alternative solutions did you explore? (Optional)

    None, clearing or diffing existing menu items in TabsContainer.updateNavigationMenuStructure before adding items is the cleanest, idiomatic solution.

    What changes do you think we should make in order to solve the problem?

    Add a patch in patches/react-native-screens/ (or update existing gamma patches, e.g. react-native-screens+4.28.0+006+clear-tab-menu-before-update.patch):

    In node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsContainer.kt:
    Before populating fragments in updateNavigationMenuStructure(), clear the existing menu:

    --- a/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsContainer.kt
    +++ b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsContainer.kt
    @@ -540,6 +540,7 @@ class TabsContainer(
         }
     
         private fun updateNavigationMenuStructure() {
    +        bottomNavigationView.menu.clear()
             fragments.forEachIndexed { index, fragment ->
                 val menuItem = getOrCreateMenuItemForFragment(fragment)
                 // configure item...

    Or in MenuHelpers.kt:
    Ensure that getOrCreateMenuItemForFragment checks bottomNavigationView.menu.findItem(fragment.id) instead of unconditionally calling menu.add(...).

    What automated tests can you add to prevent this from re-occurring?

    Add an Android instrumented/unit test for TabsContainer verifying that calling onAttachedToWindow() and flushPendingUpdates() repeatedly on a TabsContainer with 4-5 tabs never causes bottomNavigationView.menu.size() to exceed the fragment count or throw IllegalArgumentException.

  9. melvin-bot commented on Oct 21, 2026

    @melvin-bot

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

  10. sijo0218 commented on Oct 21, 2026

    @sijo0218

    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/~011091133bbb09cd1c

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    On Android HybridApp, navigating between screens (e.g. visiting sub-pages or during screen transitions in a ScreenStack) fatal crashes with:

    IllegalArgumentException: Maximum number of items supported by CustomBottomNavigationView is 6
    

    thrown from:

    com.swmansion.rnscreens.ScreenStack.onUpdate (ScreenStack.kt:323)
    com.swmansion.rnscreens.gamma.tabs.container.TabsContainer.onAttachedToWindow (TabsContainer.kt:311)
    com.swmansion.rnscreens.gamma.tabs.container.TabsContainer.flushPendingUpdates (TabsContainer.kt:212)
    com.swmansion.rnscreens.gamma.tabs.container.TabsContainer.updateNavigationMenuStructure (TabsContainer.kt:545)
    com.swmansion.rnscreens.gamma.tabs.container.MenuHelpersKt.getOrCreateMenuItemForFragment (MenuHelpers.kt:8)
    androidx.appcompat.view.menu.MenuBuilder.add
    

    What is the root cause of that problem?

    There are two interconnected defects in react-native-screens gamma tabs on Android:

    1. Child Removal Failure in Fabric (TabsHostViewManager):

      • In React Native Fabric, ViewGroupManager manages view additions and removals by inspecting child views via getChildCount(parent) and getChildAt(parent, index).
      • In TabsHostViewManager.kt, TabsHost does not attach TabsScreen views directly as Android ViewGroup children (they are managed in an internal renderedScreens list and encapsulated in TabsScreenFragment inside TabsContainer.tabsModel).
      • However, TabsHostViewManager does not override getChildCount or getChildAt. Consequently, React Native defaults to parent.childCount (which only sees the root container, count = 1).
      • When Fabric attempts to remove an inactive or toggled tab child at index > 0, getChildAt(parent, index) returns out of bounds or mismatches, causing Fabric to consider the view "already removed from parent" and skip calling removeViewAt / removeView.
      • As a result, TabsHost.unmountReactSubviewAt and TabsContainer.removeTabsScreenAt are never called for the old tab. When the tab or replacement screen is re-mounted, TabsHostViewManager.addView calls mountReactSubviewAt -> container.addTabsScreenAt, appending another fragment into tabsModel.
      • Every tab transition or beta re-render leaks stale fragments into tabsModel, causing tabsModel.size to grow beyond 5 to 7 or more.
    2. Uncapped Menu Population against Hardware Limits in updateNavigationMenuStructure():

      • In TabsContainer.kt:532-547:
        private fun updateNavigationMenuStructure() {
            val menu = bottomNavigationView.menu
            val isMenuInSync =
                menu.size == tabsModel.size &&
                    tabsModel.withIndex().all { (index, fragment) ->
                        menu.getItem(index).itemId == fragment.menuItemId
                    }
            if (isMenuInSync) return
            menu.clear()
            tabsModel.forEach { fragment ->
                menu.getOrCreateMenuItemForFragment(fragment)
            }
        }
      • Notice that menu.clear() on line 543 does clear bottomNavigationView.menu. However, tabsModel.forEach iterates over all items in tabsModel without checking bottomNavigationView.maxItemCount (MAX_ITEM_COUNT = 6 in CustomBottomNavigationView / Android Material NavigationBarMenu).
      • In TabsHost.kt:76, mountReactSubviewAt only checks index < bottomNavigationViewMaxItemCount, never checking whether renderedScreens.size >= bottomNavigationViewMaxItemCount or whether the TabsScreen is already mounted.
      • When ScreenStack.onUpdate executes (e.g. re-attaching screens in stack order via transaction.commitNowAllowingStateLoss()), TabsContainer.onAttachedToWindow calls flushPendingUpdates() -> updateNavigationMenuStructure(). Because tabsModel accumulated 7 or more fragments, tabsModel.forEach attempts to add the 7th item into bottomNavigationView.menu.
      • NavigationBarMenu.addInternal detects size() + 1 > maxItemCount (7 > 6) and throws IllegalArgumentException: Maximum number of items supported by CustomBottomNavigationView is 6.

    What alternative solutions did you explore? (Optional)

    Attempting to only cap at the JS layer (NativeBottomTabView.native.js) is insufficient because Fabric view re-ordering and remount cycles on Android will still leak native fragments inside TabsHost.renderedScreens and TabsContainer.tabsModel if TabsHostViewManager cannot report its children. Both the view manager lifecycle and native container menu bounds must be secured.

    What changes do you think we should make in order to solve the problem?

    Add a patch in patches/react-native-screens/react-native-screens+4.28.0+006+fix-tabs-host-child-lifecycle-and-menu-limit.patch:

    1. In TabsHostViewManager.kt:
      Override getChildCount and getChildAt to reflect renderedScreens so Fabric can successfully unmount child screens:

      override fun getChildCount(parent: TabsHost): Int = parent.renderedScreensCount
      
      override fun getChildAt(parent: TabsHost, index: Int): View? = parent.getRenderedScreenAt(index)
    2. In TabsHost.kt:
      Expose renderedScreensCount and getRenderedScreenAt(index):

      internal val renderedScreensCount: Int
          get() = renderedScreens.size
      
      internal fun getRenderedScreenAt(index: Int): TabsScreen? =
          renderedScreens.getOrNull(index)

      In mountReactSubviewAt, guard against duplicate insertion and ensure renderedScreens deduplicates any existing entry for the same screen:

      internal fun mountReactSubviewAt(tabsScreen: TabsScreen, index: Int) {
          unmountReactSubview(tabsScreen)
          val maxCount = container.bottomNavigationView.maxItemCount
          require(renderedScreens.size < maxCount) {
              "[RNScreens] Cannot mount TabsScreen: container already reached maximum capacity of $maxCount items"
          }
          renderedScreens.add(index.coerceAtMost(renderedScreens.size), tabsScreen)
          tabsScreen.setTabsScreenDelegate(container)
          container.addTabsScreenAt(index.coerceAtMost(renderedScreens.size - 1), tabsScreen)
      }
    3. In TabsContainer.kt:
      In addTabsScreenAt, deduplicate matching screens:

      internal fun addTabsScreenAt(index: Int, tabsScreen: TabsScreen) {
          tabsModel.removeIf { it.tabsScreen === tabsScreen || it.requireScreenKey == tabsScreen.requireScreenKey }
          tabsModel.add(index.coerceAtMost(tabsModel.size), TabsScreenFragment(tabsScreen))
          invalidationFlags.invalidateAll()
      }

      In updateNavigationMenuStructure(), bound the iteration strictly to bottomNavigationView.maxItemCount:

      private fun updateNavigationMenuStructure() {
          val menu = bottomNavigationView.menu
          val isMenuInSync =
              menu.size == tabsModel.size &&
                  tabsModel.withIndex().all { (index, fragment) ->
                      menu.getItem(index).itemId == fragment.menuItemId
                  }
      
          if (isMenuInSync) {
              return
          }
          menu.clear()
          val maxItems = bottomNavigationView.maxItemCount
          tabsModel.take(maxItems).forEach { fragment ->
              menu.getOrCreateMenuItemForFragment(fragment)
          }
      }

    What automated tests can you add to prevent this from re-occurring?

    1. In Android unit/integration tests (TabsContainerTest / TabsHostTest), verify that repeatedly mounting, unmounting, and re-mounting tabs on TabsHost with Fabric view manager dispatches:
      • Maintains renderedScreens.size == tabsModel.size.
      • Never exceeds bottomNavigationView.maxItemCount.
      • Never throws IllegalArgumentException during onAttachedToWindow() and flushPendingUpdates().
    2. In tests/unit/NativeBottomTabViewTest.tsx, add test cases simulating rapid navigation transitions across tab routes and beta toggles to ensure the number of native Tabs.Screen children mounted to Tabs.Host never exceeds 5.
  11. melvin-bot commented on Oct 21, 2026

    @melvin-bot

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

  12. 11 remaining items

  13. melvin-bot commented on Oct 23, 2026

    @melvin-bot

    Current assignee @mountiny is eligible for the choreEngineerContributorManagement assigner, not assigning anyone new.

  14. yusufdeveloper2903 commented on Oct 23, 2026

    @yusufdeveloper2903
    Contributor

    Proposal

    What is the root cause of that problem?

    The App has six tab routes; the native bar shows five. The tab without a bar item gets no Tabs.Screen, so when that tab changes (the Insights beta loading after launch or being toggled), React removes one Tabs.Screen from the native host and inserts another. Before #104238 a re-keyed focused tab route (markFocusedTabRouteForRemount) did the same; both triggers were live in 9.5.13-0:

    tabBarTintColor: activeTintColor
    },
    children: state.routes.map((route, index) => {
    + if (isRouteHidden(route)) {
    + return null;
    + }

    On Android the removal never reaches react-native-screens. TabsHost keeps its tabs in renderedScreens and hands them to TabsContainer as fragments, so its only real Android child is the container. TabsHostViewManager implements removeViewAt but not getChildAt/getChildCount:

    https://github.com/software-mansion/react-native-screens/blob/42788c197e817ebfe21c0f5d36e0b0e02df4950e/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHostViewManager.kt#L50-L59

    Fabric checks the child with getChildAt before removing it. The default reads the raw Android children, does not find the tab, logs view already removed from parent! and returns, so TabsHost.unmountReactSubviewAt, which drops the fragment from tabsModel, never runs. Inserts have no such check:

    https://github.com/facebook/react-native/blob/72258ddf7b0b795bbafcbeb5930620b6500c7a06/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt#L456-L482

    Each remove + insert leaves one stale fragment. updateNavigationMenuStructure clears the menu and adds one item per tabsModel entry, so the second change makes 7 and MenuBuilder.add throws. While a page covers the tabs the container is detached and the flush waits for onAttachedToWindow, which is why the stack starts in ScreenStack.onUpdate:

    https://github.com/software-mansion/react-native-screens/blob/42788c197e817ebfe21c0f5d36e0b0e02df4950e/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsContainer.kt#L532-L547

    Main now avoids both triggers in JS: the route-name tab key from #104238 (staging 9.5.13-1) and a host remount when the hidden tab changes from #104368 (staging 9.5.16-0, not on production yet). The Android removal itself is unchanged, as the patch 004 note says:

    ### [@react-navigation+bottom-tabs+7.19.2+004+stable-native-tab-identity.patch](@react-navigation+bottom-tabs+7.19.2+004+stable-native-tab-identity.patch)
    - Reason: Identifies each native tab by its route name instead of its route key. The App gives the focused tab route a new key on purpose when it pre-inserts a screen under the RHP (`markFocusedTabRouteForRemount` in `GetStateForActionHandlers.ts`, #90985), so the split navigator remounts without a push-transition flash. Keyed by route key, that new key replaced the whole `Tabs.Screen`. Android never removes the old one: `TabsHostViewManager` does not override `getChildAt`/`getChildCount`, so Fabric's `removeViewAt` finds no child at that index ("view already removed from parent!") and skips `TabsHost.unmountReactSubviewAt`, while the insert goes through. The bar then shows the tab twice, and the stale copy opens a blank page. With this patch the native tab (`key`, `screenKey`, `navStateRequest.selectedScreenKey` and the selection events) uses the route name, which stays the same, and only the tab's content is keyed by the route key, so a re-keyed route still remounts its content inside the same native tab. **Remove this patch** once react-native-screens removes Android tabs correctly and React Navigation's native tab view no longer recreates a tab when its route key changes.
    - Upstream PR/issue: not reported yet. The Android removal bug belongs to react-native-screens (`TabsHostViewManager`).
    - E/App issue: [#104230](https://github.com/Expensify/App/issues/104230), [#104233](https://github.com/Expensify/App/issues/104233)
    - PR Introducing Patch: [#104238](https://github.com/Expensify/App/pull/104238)
    - PR Updating Patch: N/A

    // The native tab host can't drop a tab it already shows: on Android the removed tab keeps its bar item and keeps
    // firing selection events. Remounting the host when the set of tabs without a bar item changes (the Insights beta
    // loading after launch or being toggled) gives it only the tabs it should show.
    const tabsWithoutBarItemKey = state.routes
    .filter((route) => descriptors[route.key]?.options.tabBarItemHidden)
    .map((route) => route.name)
    .join();
    // The tabs stay at the same place in the tree in both layouts, so crossing the breakpoint does not remount them.
    return (
    <View style={[styles.flex1, !shouldUseNarrowLayout && styles.flexRow]}>
    {!shouldUseNarrowLayout && <TabNavigatorBar state={state} />}
    <View
    key={tabsWithoutBarItemKey}
    style={styles.flex1}
    >

    I reproduced it on dev Android with that remount key removed (the JS production 9.5.13-6 onward still ships) and watched logcat:

    • Account tab, Troubleshoot > Beta overrides, insightsPage on: removeViewAt: [2542] -> [2544] @4: view already removed from parent!
    • insightsPage off: removeViewAt: [6324] -> [2544] @3: view already removed from parent!
    • Back to the tabs: Maximum number of items supported by CustomBottomNavigationView is 6 from MenuHelpers.kt:8 ← TabsContainer.kt:545 ← :212 ← onAttachedToWindow :311 ← ScreenStack.onUpdate (ScreenStack.kt:323), the APP-N5H frames.

    On unchanged main the toggles do not crash, but logcat still shows every removal skipped.

    What changes do you think we should make in order to solve the problem?

    Add react-native-screens+4.28.0+007 so TabsHostViewManager reports renderedScreens to Fabric, as the gamma stack host already does:

    https://github.com/software-mansion/react-native-screens/blob/42788c197e817ebfe21c0f5d36e0b0e02df4950e/android/src/main/java/com/swmansion/rnscreens/gamma/stack/host/StackHostViewManager.kt#L52-L57

    -    private val renderedScreens: ArrayList<TabsScreen> = arrayListOf()
    +    internal val renderedScreens: ArrayList<TabsScreen> = arrayListOf()
    ...
    +    override fun getChildAt(parent: TabsHost, index: Int): View? = parent.renderedScreens.getOrNull(index)
    +    override fun getChildCount(parent: TabsHost): Int = parent.renderedScreens.size

    Fabric then reaches unmountReactSubviewAt, and tabsModel matches what JS renders on every remove path. I'd keep the remount and patch 004: TabsContainer.selectedTab is a checkNotNull lookup, so removing the selected tab from a live host still needs them. I have not built the patched APK yet.

    What alternative solutions did you explore? (Optional)

    • Stop adding menu items at 6 in getOrCreateMenuItemForFragment: hides the crash but leaves the stale tab in tabsModel.
  15. nabi-ebrahimi commented on Oct 23, 2026

    @nabi-ebrahimi
    Contributor

    Proposal

    What is the root cause of that problem?

    Android skips native tab removals, so stale tabs accumulate in TabsContainer.tabsModel until rebuilding the menu exceeds its six-item limit.

    TabsHost stores its logical tab children in renderedScreens; its only direct Android child is the internal container. However, TabsHostViewManager implements additions and removals without overriding getChildAt or getChildCount. Fabric therefore inspects the wrong children, cannot find the tab’s tag, and returns before calling removeViewAt. ([TabsHost](https://github.com/software-mansion/react-native-screens/blob/2381ed8d72fe19185fc8415cb61df7b79bbf4376/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHost.kt#L32-L64), [view manager](https://github.com/software-mansion/react-native-screens/blob/2381ed8d72fe19185fc8415cb61df7b79bbf4376/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHostViewManager.kt#L33-L59), [Fabric removal check](https://github.com/react/react-native/blob/a632f9efe24bac8b3a113c78469948f55bde0f5d/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt#L453-L505))

    Replacement tabs still reach mountReactSubviewAt, while the old tabs never reach unmountReactSubviewAt. The insertion guard checks the insertion index, not the accumulated count. Starting with five native tabs, two replacements can therefore leave seven entries. ([Mount/unmount implementation](https://github.com/software-mansion/react-native-screens/blob/2381ed8d72fe19185fc8415cb61df7b79bbf4376/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHost.kt#L85-L104))

    updateNavigationMenuStructure already clears the menu, then adds one item per model entry. The seventh entry causes the exception. Updates wait while the container is detached and resume in onAttachedToWindow, explaining the reattachment path in the reported stack. Reattachment without accumulated stale tabs does not increase the menu count. ([Menu rebuild](https://github.com/software-mansion/react-native-screens/blob/2381ed8d72fe19185fc8415cb61df7b79bbf4376/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsContainer.kt#L532-L547), [attachment handling](https://github.com/software-mansion/react-native-screens/blob/2381ed8d72fe19185fc8415cb61df7b79bbf4376/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/container/TabsContainer.kt#L291-L311))

    The App has six routes but renders five native tabs: the hidden-tab patch omits Tabs.Screen for the hidden route. Earlier builds replaced native tabs when a focused route received a new key or the Insights beta changed which tab was hidden. Current main mitigates these triggers with [stable native tab identity](

    ### [@react-navigation+bottom-tabs+7.19.2+004+stable-native-tab-identity.patch](@react-navigation+bottom-tabs+7.19.2+004+stable-native-tab-identity.patch)
    - Reason: Identifies each native tab by its route name instead of its route key. The App gives the focused tab route a new key on purpose when it pre-inserts a screen under the RHP (`markFocusedTabRouteForRemount` in `GetStateForActionHandlers.ts`, #90985), so the split navigator remounts without a push-transition flash. Keyed by route key, that new key replaced the whole `Tabs.Screen`. Android never removes the old one: `TabsHostViewManager` does not override `getChildAt`/`getChildCount`, so Fabric's `removeViewAt` finds no child at that index ("view already removed from parent!") and skips `TabsHost.unmountReactSubviewAt`, while the insert goes through. The bar then shows the tab twice, and the stale copy opens a blank page. With this patch the native tab (`key`, `screenKey`, `navStateRequest.selectedScreenKey` and the selection events) uses the route name, which stays the same, and only the tab's content is keyed by the route key, so a re-keyed route still remounts its content inside the same native tab. **Remove this patch** once react-native-screens removes Android tabs correctly and React Navigation's native tab view no longer recreates a tab when its route key changes.
    - Upstream PR/issue: not reported yet. The Android removal bug belongs to react-native-screens (`TabsHostViewManager`).
    - E/App issue: [#104230](https://github.com/Expensify/App/issues/104230), [#104233](https://github.com/Expensify/App/issues/104233)
    - PR Introducing Patch: [#104238](https://github.com/Expensify/App/pull/104238)
    - PR Updating Patch: N/A
    ) and a [host remount when the hidden tab changes](
    // The native tab host can't drop a tab it already shows: on Android the removed tab keeps its bar item and keeps
    // firing selection events. Remounting the host when the set of tabs without a bar item changes (the Insights beta
    // loading after launch or being toggled) gives it only the tabs it should show.
    const tabsWithoutBarItemKey = state.routes
    .filter((route) => descriptors[route.key]?.options.tabBarItemHidden)
    .map((route) => route.name)
    .join();
    // The tabs stay at the same place in the tree in both layouts, so crossing the breakpoint does not remount them.
    return (
    <View style={[styles.flex1, !shouldUseNarrowLayout && styles.flexRow]}>
    {!shouldUseNarrowLayout && <TabNavigatorBar state={state} />}
    <View
    key={tabsWithoutBarItemKey}
    style={styles.flex1}
    >
    {children}
    ). The native removal defect remains.

    What changes do you think we should make in order to solve the problem?

    Add a sequenced react-native-screens patch that exposes read-only accessors for renderedScreens and overrides TabsHostViewManager.getChildCount and getChildAt to use them. Follow the existing [gamma stack host implementation](https://github.com/software-mansion/react-native-screens/blob/2381ed8d72fe19185fc8415cb61df7b79bbf4376/android/src/main/java/com/swmansion/rnscreens/gamma/stack/host/StackHostViewManager.kt#L52-L57), including nullable lookup for an invalid index.

    Fabric can then find the intended child and execute the existing removal, keeping renderedScreens and tabsModel synchronized with React.

    Keep the current stable-identity patch and host-remount behavior. Correct child reporting alone does not make removing the selected tab safe: selectedTab requires that tab to remain in the model.

    Validate the native boundary with repeated non-selected tab replacements while the host survives, including replacements while detached followed by reattachment. Assert that removed tags disappear and the model/menu retain the expected count. Also verify Android HybridApp expense creation and Insights-beta changes with the existing mitigations retained; mocked JavaScript tab tests cannot exercise Fabric’s removal check.

    What alternative solutions did you explore? (Optional)

    Clearing the menu is already implemented. Capping menu population would leave stale tabs in the model, while rejecting further insertions would merely move the crash earlier. Correcting child reporting addresses the failed removal itself.

  16. bconnnnn commented on Oct 23, 2026

    @bconnnnn

    Proposal

    What is the root cause of that problem?

    The stack trace shows a seventh native menu entry being added despite Android's six-item limit. A duplicate during tab-container reattachment is plausible but not yet reproduced.

    What changes do you think we should make in order to solve the problem?

    Instrument tab IDs and native menu counts during attach/update; determine duplicate insertion versus seven distinct tabs, then reconcile entries correctly instead of suppressing the exception.

    What alternative solutions did you explore? (Optional)

    Avoid a UI-only workaround that masks the symptom without fixing the state or lifecycle problem.

    Validation: Test repeated attach/detach and tab switching, including Settings_Troubleshoot.

    @mananjadhav I'm available if you need a contributor for this issue. I'll wait for approval before opening a PR.

  17. removed
    Help WantedApply this label when an issue is open to proposals by contributors
    on Oct 23, 2026
  18. melvin-bot commented on Oct 23, 2026

    @melvin-bot

    ❌ There was an error making the offer to @wildan-m for the Contributor role. The BZ member will need to manually hire the contributor.

  19. wildan-m commented on Oct 26, 2026

    @wildan-m
    Contributor

    on it

  20. wildan-m commented on Oct 26, 2026

    @wildan-m
    Contributor

    @mananjadhav the PR is ready #105020

  21. Kabeer099 commented on Oct 29, 2026

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

Metadata

Metadata

Labels

DailyKSv2ExternalAdded to denote the issue can be worked on by a contributorReviewingHas a PR in review

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions