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
[Exploratory] FAB button is lost when navigate back from Concierge RHP to Inbox #100521
On narrow layout the FAB is rendered only by the navigator-level tab bar. Every tab root screen also renders a second, duplicate tab bar through ScreenWrapper's bottomContent, and that copy hardcodes shouldShowFloatingButtons={false}. So "tab bar is there but the FAB is gone" means the navigator-level bar is stuck hidden and you are looking at the duplicate.
It gets stuck because isTabRouteAtRoot falls back to the tab route's params whenever the route's nested state is undefined:
Opening Concierge sets the Inbox tab route's params to the push target Report.
A back navigation pops the nested stack but never clears those tab-level params — the file's own comment says they "can be stale after within-tab back navigation".
The tab navigator runs with lazy: true and freezeOnBlur: true on native. React Navigation blanks a frozen screen's cached state back to undefined, which is what the first half of the repro (Spend → Concierge → Back → switch tabs) sets up.
What changes do you think we should make in order to solve the problem?
Stop trusting params once a route has hydrated. The push-target fallback exists only so the bar doesn't flash on Android cold start, so scope it to that: track per route.key whether a real nested state has ever been observed, and after the first observation resolve isAtRoot from the last known focused leaf instead of falling back to getPushTargetLeaf(route.params). A frozen or not-yet-rendered tab then keeps its last correct answer rather than resurrecting a screen the user already backed out of.
What alternative solutions did you explore? (Optional)
Clear the tab route's params when a within-tab back navigation pops the nested stack. Fixes the source of the staleness but touches the custom routers, so it is riskier.
Turn off freezeOnBlur for the tab navigator. Removes the trigger but gives up the performance win.
Investigation details and what I could not verify
Ruled out — the animationDoneKey / shouldApplyDelay gate.usePrevious(shouldHide) clears shouldApplyDelay on the very next render, and the requestAnimationFrame effect re-runs on every stateKey change, so the delay is transient by construction. isHidden can only latch if shouldHide latches.
Ruled out — isBlockingViewVisible. Its only writer is ForceFullScreenView, and the native variant is a pure passthrough that never registers a route key, so the value is permanently false on iOS.
Repro asymmetry explained. Opening Concierge from Spend crosses navigators and pushes a whole new TAB_NAVIGATOR, so backing out changes state.index and the bar recovers. Opening it from Inbox stays in the same tab route, which is why only the second pass shows the bug.
Not verified at runtime. I could not confirm on a device that activeRoute.state is actually undefined at that moment. The mechanism depends on native freeze behavior, so a web run cannot validate it (my web session also crashed to a blank page before reaching the flow). The quickest confirmation: enable debug mode on the device — DebugTabView already displays isTabRouteAtRoot — or log {index, params, state} from TabNavigatorBar.
Confidence: Medium. The code path is fully traced, but it hinges on the unverified runtime assumption above.
Related issues: a quick scan of open Expensify/App issues found no duplicate or closely related report.
Next Steps for Contributor+ team: To accept:@MelvinBot implement [this](https://github.com/Expensify/App/issues/100521) 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 narrow/mobile screens (iOS/Android), after opening Concierge from the Spend tab, backing out, switching to the Inbox tab, opening Concierge again, and backing out to Inbox, the floating action button (FAB) disappears from the screen.
What is the root cause of that problem?
In narrow layout, the FAB is exclusively rendered by the navigator-level tab bar (TabNavigatorBar.tsx). Individual tab root screens also render a bottom tab bar via ScreenWrapper.bottomContent, but that duplicate copy hardcodes shouldShowFloatingButtons={false}. When the FAB disappears while the tab bar is visible, it means the navigator-level tab bar is hidden (shouldHide = true), leaving only the duplicate bottom content visible.
The navigator-level bar hides when isTabRouteAtRoot(activeRoute) returns false.
In src/libs/Navigation/helpers/isTabRouteAtRoot.ts:55-58:
When opening Concierge from a tab, route.params receives the push target params (e.g. {screen: 'Report', params: ...}).
When the user navigates back, the nested navigation stack pops back to root, but React Navigation does not clear route.params (as noted in the file: "Tab-level params can be stale after within-tab back navigation").
On native platforms, the tab navigator is configured with freezeOnBlur: true. When a tab is unfocused and refocused, React Navigation freezes the screen and can reset route.state to undefined before rehydration.
When route.state is undefined, focusedLeaf evaluates to undefined.
isTabRouteAtRoot falls back to getPushTargetLeaf(route?.params). Since route.params still carries the stale push target (Report), and Report is not in SCREENS_WITH_TAB_BAR, isAtTabRootLevel evaluates to false.
As a result, isAtRoot becomes false, shouldHide becomes true, and TabNavigatorBar hides the navigator tab bar, removing the FAB.
What changes do you think we should make in order to solve the problem?
In src/libs/Navigation/helpers/isTabRouteAtRoot.ts:
Track the last known focused leaf by route.key so that once a tab route has hydrated and resolved its root state, a temporary route.state === undefined (e.g. from freezeOnBlur) retains the last resolved state rather than resurrecting stale push params:
The fallback to getPushTargetLeaf(route?.params) is reserved strictly for initial cold starts before the route's navigator state has ever rendered. Once the user has navigated and popped back, lastKnownFocusedLeafByRouteKey preserves the current root view leaf.
What alternative solutions did you explore? (Optional)
Disabling freezeOnBlur: true in TabNavigator.native.tsx was considered, but that forfeits native memory optimization and background view suspension.
Manually stripping route.params on back navigation inside custom routers was explored, but mutating route params during stack unwinds carries broader side effects for deep linking.
Tracking the last resolved leaf for the route key is lightweight, self-contained within isTabRouteAtRoot.ts, and prevents stale params from falsely hiding the FAB.
Thanks eh2077. I rechecked this against current main (9.5.4-0). I think the bug is most likely already gone, and I suggest you don't implement my earlier proposal.
No PR fixed it directly. The FAB visibility code, the Concierge button's navigation, and the Back path are basically the same as in 9.4.73-0, where the bug was reported. The route.params fallback is still in isTabRouteAtRoot.ts:55-58.
My earlier root cause is still unconfirmed. When I traced the code again, I found no path that leaves the Inbox tab with no navigation state, in either version. Treat the proposal as unverified.
Suggested next step: ask Applause to retest on a current build on a physical iPhone. If they can't reproduce it, close the issue. The reporter used a real iPhone 11 on iOS 26.2, so your simulator result alone doesn't fully rule the bug out.
@Expensify/applauseleads please retest this on the latest build, on a physical iPhone if possible.
eh2077 can't reproduce it on an iOS simulator, and the bug may have been fixed as a side effect of a later change. Please use the steps in the issue description and confirm whether the FAB still disappears after step 5.
Activity
applause-bot commentedon Sep 7, 2026
While Applause doesn't embed attachments for this bug, here are the links for quick view:
https://platform.applause.com/services/links/v1/external/1f695e6016a7a730a1f967af6ffbdd277ee29bba359b3458b07e45ece5821f19
MelvinBot commentedon Sep 24, 2026
Proposal
What is the root cause of that problem?
On narrow layout the FAB is rendered only by the navigator-level tab bar. Every tab root screen also renders a second, duplicate tab bar through
ScreenWrapper'sbottomContent, and that copy hardcodesshouldShowFloatingButtons={false}. So "tab bar is there but the FAB is gone" means the navigator-level bar is stuck hidden and you are looking at the duplicate.It gets stuck because
isTabRouteAtRootfalls back to the tab route'sparamswhenever the route's nestedstateisundefined:Report.lazy: trueandfreezeOnBlur: trueon native. React Navigation blanks a frozen screen's cachedstateback toundefined, which is what the first half of the repro (Spend → Concierge → Back → switch tabs) sets up.With
stateundefined and params still sayingReport— a screen that is not in the tab-root allow-list —isAtRootisfalse, soshouldHidestaystrueand the bar is rendered at opacity 0 withshouldShowFloatingButtons={false}.This is the remaining hole in [CP Staging] fix: always show FAB on the root-level, which made the focused leaf authoritative but kept the stale-params fallback for the
state === undefinedcase.What changes do you think we should make in order to solve the problem?
Stop trusting
paramsonce a route has hydrated. The push-target fallback exists only so the bar doesn't flash on Android cold start, so scope it to that: track perroute.keywhether a real nestedstatehas ever been observed, and after the first observation resolveisAtRootfrom the last known focused leaf instead of falling back togetPushTargetLeaf(route.params). A frozen or not-yet-rendered tab then keeps its last correct answer rather than resurrecting a screen the user already backed out of.What alternative solutions did you explore? (Optional)
paramswhen a within-tab back navigation pops the nested stack. Fixes the source of the staleness but touches the custom routers, so it is riskier.freezeOnBlurfor the tab navigator. Removes the trigger but gives up the performance win.Investigation details and what I could not verify
Ruled out — the
animationDoneKey/shouldApplyDelaygate.usePrevious(shouldHide)clearsshouldApplyDelayon the very next render, and therequestAnimationFrameeffect re-runs on everystateKeychange, so the delay is transient by construction.isHiddencan only latch ifshouldHidelatches.Ruled out —
isBlockingViewVisible. Its only writer isForceFullScreenView, and the native variant is a pure passthrough that never registers a route key, so the value is permanentlyfalseon iOS.Repro asymmetry explained. Opening Concierge from Spend crosses navigators and pushes a whole new
TAB_NAVIGATOR, so backing out changesstate.indexand the bar recovers. Opening it from Inbox stays in the same tab route, which is why only the second pass shows the bug.Not verified at runtime. I could not confirm on a device that
activeRoute.stateis actuallyundefinedat that moment. The mechanism depends on native freeze behavior, so a web run cannot validate it (my web session also crashed to a blank page before reaching the flow). The quickest confirmation: enable debug mode on the device —DebugTabViewalready displaysisTabRouteAtRoot— or log{index, params, state}fromTabNavigatorBar.Confidence: Medium. The code path is fully traced, but it hinges on the unverified runtime assumption above.
Related issues: a quick scan of open
Expensify/Appissues found no duplicate or closely related report.Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/100521)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
view run · no recording available
melvin-bot commentedon Sep 24, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @eh2077 (
External)melvin-bot commentedon Sep 24, 2026
Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.
ashusatyarthi-wq commentedon Sep 26, 2026
Contributor details
Your Expensify account email: ashu.satyarthi.business@gmail.com Upwork Profile Link: https://www.upwork.com/freelancers/~01b8ab177d61dc225cProposal
Please re-state the problem that we are trying to solve in this issue.
On narrow/mobile screens (iOS/Android), after opening Concierge from the Spend tab, backing out, switching to the Inbox tab, opening Concierge again, and backing out to Inbox, the floating action button (FAB) disappears from the screen.
What is the root cause of that problem?
In narrow layout, the FAB is exclusively rendered by the navigator-level tab bar (
TabNavigatorBar.tsx). Individual tab root screens also render a bottom tab bar viaScreenWrapper.bottomContent, but that duplicate copy hardcodesshouldShowFloatingButtons={false}. When the FAB disappears while the tab bar is visible, it means the navigator-level tab bar is hidden (shouldHide = true), leaving only the duplicate bottom content visible.The navigator-level bar hides when
isTabRouteAtRoot(activeRoute)returnsfalse.In
src/libs/Navigation/helpers/isTabRouteAtRoot.ts:55-58:route.paramsreceives the push target params (e.g.{screen: 'Report', params: ...}).route.params(as noted in the file: "Tab-level params can be stale after within-tab back navigation").freezeOnBlur: true. When a tab is unfocused and refocused, React Navigation freezes the screen and can resetroute.statetoundefinedbefore rehydration.route.stateisundefined,focusedLeafevaluates toundefined.isTabRouteAtRootfalls back togetPushTargetLeaf(route?.params). Sinceroute.paramsstill carries the stale push target (Report), andReportis not inSCREENS_WITH_TAB_BAR,isAtTabRootLevelevaluates tofalse.isAtRootbecomesfalse,shouldHidebecomestrue, andTabNavigatorBarhides the navigator tab bar, removing the FAB.What changes do you think we should make in order to solve the problem?
In
src/libs/Navigation/helpers/isTabRouteAtRoot.ts:Track the last known focused leaf by
route.keyso that once a tab route has hydrated and resolved its root state, a temporaryroute.state === undefined(e.g. fromfreezeOnBlur) retains the last resolved state rather than resurrecting stale push params:The fallback to
getPushTargetLeaf(route?.params)is reserved strictly for initial cold starts before the route's navigator state has ever rendered. Once the user has navigated and popped back,lastKnownFocusedLeafByRouteKeypreserves the current root view leaf.What alternative solutions did you explore? (Optional)
freezeOnBlur: trueinTabNavigator.native.tsxwas considered, but that forfeits native memory optimization and background view suspension.route.paramson back navigation inside custom routers was explored, but mutating route params during stack unwinds carries broader side effects for deep linking.Tracking the last resolved leaf for the route key is lightweight, self-contained within
isTabRouteAtRoot.ts, and prevents stale params from falsely hiding the FAB.melvin-bot commentedon Sep 29, 2026
@eh2077 Huh... This is 4 days overdue. Who can take care of this?
4 remaining items
melvin-bot commentedon Oct 6, 2026
@eh2077 Huh... This is 4 days overdue. Who can take care of this?
eh2077 commentedon Oct 7, 2026
I can't reproduce with iOS simulator. @MelvinBot check this issue again please
Screen.Recording.2026-10-07.at.9.58.19.PM.mov
MelvinBot commentedon Oct 7, 2026
Thanks
eh2077. I rechecked this against current main (9.5.4-0). I think the bug is most likely already gone, and I suggest you don't implement my earlier proposal.route.paramsfallback is still inisTabRouteAtRoot.ts:55-58.AuthScreensInitHandler.tsx:256-276). My earlier analysis depended on the Inbox tab having no navigation state yet, and the preload removes that condition.Suggested next step: ask Applause to retest on a current build on a physical iPhone. If they can't reproduce it, close the issue. The reporter used a real iPhone 11 on iOS 26.2, so your simulator result alone doesn't fully rule the bug out.
Other changes since 9.4.73-0 that I ruled out
sourceReportIDparameter, and that parameter is empty for both taps in this repro.@react-navigation/*versions haven't changed.I didn't run a web test, because this bug depends on native iOS screen freezing and web can't reproduce it.
view run
melvin-bot commentedon Oct 12, 2026
@eh2077 Eep! 4 days overdue now. Issues have feelings too...
eh2077 commentedon Oct 14, 2026
@MelvinBot since it isn't reproducible anymore, can you ask QA team for retest, then we can close it more confidently!
MelvinBot commentedon Oct 14, 2026
@Expensify/applauseleads please retest this on the latest build, on a physical iPhone if possible.
eh2077can't reproduce it on an iOS simulator, and the bug may have been fixed as a side effect of a later change. Please use the steps in the issue description and confirm whether the FAB still disappears after step 5.If you can't reproduce it, we'll close the issue.
view run
melvin-bot commentedon Oct 19, 2026
@eh2077 Eep! 4 days overdue now. Issues have feelings too...
eh2077 commentedon Oct 20, 2026
Not reproducible #100521 (comment), closing