I have a following Expo Router folder structure
- /(tabs)
- _layout.tsx
- home
- messages.tsx
- qr.tsx
- transactions.tsx
- /profile
- _layout.tsx
- account.tsx
- security.tsx
In general, the navigation works fine, when navigating forward but can cause unwanted issues when navigating backwards.
If a user is currently for example on "Profile > Account" screen, he can navigate to "Profile" screen by tapping on Back button. But if the user instead of tapping on back button taps on Profile tab in the Tab toolbar again, then there is a strange behavior of the Navigation. It shortly switches to actual "Profile" screen but still shows the "Account" screen. If the Profile tab is tapped again, then the "Profile" screen is displayed, but from this point on the user can "navigate back" to the "Account" screen. The reason behind all this is I guess because the navigation still goes forward when going to the "Profile" screen.
The desired behavior is that if a user taps on Profile tab in tab toolbar when he is currently in a sub-screen like "Account" it should be the same behavior as pressing on back button. I don't know how to prevent the default behavior and manipulate the navigation in such a way. The closest example of such behavior would be the Facebook iOS app. There exactly this is achieved.
/(tabs)/_layout.tsx
export default function TabsLayout() {
const { bottom } = useSafeAreaInsets()
return (
<Tabs
initialRouteName="home"
screenOptions={{
tabBarHideOnKeyboard: true,
tabBarStyle: [$tabBar, { height: 40 }],
tabBarActiveTintColor: colors.text,
tabBarInactiveTintColor: colors.text,
tabBarLabelStyle: $tabBarLabel,
}}
>
<Tabs.Screen
name="home"
options={{
href: "/home",
headerShown: false,
tabBarAccessibilityLabel: translate("tabsNavigator:homeTab"),
tabBarLabel: translate("tabsNavigator:homeTab"),
tabBarIcon: ({ focused }) => (
<MaterialCommunityIcons
name="home"
color={focused ? colors.palette.accent600 : colors.palette.neutral600}
size={tabBarIconSize}
/>
),
}}
/>
<Tabs.Screen
name="messages"
options={{
href: "/messages",
headerShown: false,
tabBarAccessibilityLabel: translate("tabsNavigator:messagesTab"),
tabBarLabel: translate("tabsNavigator:messagesTab"),
tabBarIcon: ({ focused }) => (
<MaterialCommunityIcons
name="message-bulleted"
color={focused ? colors.palette.accent600 : colors.palette.neutral600}
size={tabBarIconSize}
/>
),
}}
/>
<Tabs.Screen
name="qr"
options={{
href: "/qr",
headerShown: false,
tabBarAccessibilityLabel: translate("tabsNavigator:QRTab"),
tabBarLabel: translate("tabsNavigator:QRTab"),
tabBarIcon: ({ focused }) => (
<View style={$tabBarIconQRCode}>
<MaterialCommunityIcons
name="qrcode-scan"
color={focused ? colors.palette.accent600 : colors.palette.neutral600}
size={tabBarIconSize}
/>
</View>
),
tabBarIconStyle: {
transform: `translateY(${-tabBarIconSize / 2}px)`,
},
}}
/>
<Tabs.Screen
name="transactions"
options={{
href: "/transactions",
headerShown: true,
headerTitle: "Transactions history",
headerShadowVisible: true,
tabBarAccessibilityLabel: translate("tabsNavigator:transactionsTab"),
tabBarLabel: translate("tabsNavigator:transactionsTab"),
tabBarIcon: ({ focused }) => (
<MaterialCommunityIcons
name="transfer"
color={focused ? colors.palette.accent600 : colors.palette.neutral600}
size={tabBarIconSize}
/>
),
}}
/>
<Tabs.Screen
name="profile"
options={{
href: "/profile",
headerShown: false,
headerTitle: "Profile",
tabBarAccessibilityLabel: translate("tabsNavigator:profileTab"),
tabBarLabel: translate("tabsNavigator:profileTab"),
tabBarIcon: ({ focused }) => (
<MaterialCommunityIcons
name="account-circle"
color={focused ? colors.palette.accent600 : colors.palette.neutral600}
size={tabBarIconSize}
/>
),
}}
/>
</Tabs>
)
}
/(tabs)/profile/_layout.tsx
export default function ProfileLayout() {
return (
<Stack
screenOptions={{
headerShown: true,
}}
/>
)
}
Any thoughts on how this could be achieved?
If anybody is ever interested in such a solution, I've got it to work. The solution is that you have to define the <Tabs.Screen />
in the _layout
of the path itself. Apart from that you need to turn of the lazy
option because otherwise it doesn't generate the tab properly.
export default function TabsLayout() {
const { bottom } = useSafeAreaInsets()
return (
<Tabs
initialRouteName="home"
screenOptions={{
lazy: false,
tabBarHideOnKeyboard: true,
tabBarStyle: [$tabBar, { height: 40 }],
tabBarActiveTintColor: colors.text,
tabBarInactiveTintColor: colors.text,
tabBarLabelStyle: $tabBarLabel,
}}
>
<Tabs.Screen
name="home"
options={{
href: "/home",
headerShown: false,
lazy: true,
tabBarAccessibilityLabel: translate("tabsNavigator:homeTab"),
tabBarLabel: translate("tabsNavigator:homeTab"),
tabBarIcon: ({ focused }) => (
<MaterialCommunityIcons
name="home"
color={focused ? colors.palette.accent600 : colors.palette.neutral600}
size={tabBarIconSize}
/>
),
}}
/>
<Tabs.Screen
name="messages"
options={{
href: "/messages",
headerShown: false,
lazy: true,
tabBarAccessibilityLabel: translate("tabsNavigator:messagesTab"),
tabBarLabel: translate("tabsNavigator:messagesTab"),
tabBarIcon: ({ focused }) => (
<MaterialCommunityIcons
name="message-bulleted"
color={focused ? colors.palette.accent600 : colors.palette.neutral600}
size={tabBarIconSize}
/>
),
}}
/>
<Tabs.Screen
name="qr"
options={{
href: "/qr",
headerShown: false,
lazy: true,
tabBarAccessibilityLabel: translate("tabsNavigator:QRTab"),
tabBarLabel: translate("tabsNavigator:QRTab"),
tabBarIcon: ({ focused }) => (
<View style={$tabBarIconQRCode}>
<MaterialCommunityIcons
name="qrcode-scan"
color={focused ? colors.palette.accent600 : colors.palette.neutral600}
size={tabBarIconSize}
/>
</View>
),
tabBarIconStyle: {
transform: `translateY(${-tabBarIconSize / 2}px)`,
},
}}
/>
<Tabs.Screen
name="transactions"
options={{
href: "/transactions",
headerShown: true,
headerTitle: "Transactions history",
headerShadowVisible: true,
lazy: true,
tabBarAccessibilityLabel: translate("tabsNavigator:transactionsTab"),
tabBarLabel: translate("tabsNavigator:transactionsTab"),
tabBarIcon: ({ focused }) => (
<MaterialCommunityIcons
name="transfer"
color={focused ? colors.palette.accent600 : colors.palette.neutral600}
size={tabBarIconSize}
/>
),
}}
/>
</Tabs>
)
}
export default function ProfileLayout() {
return (
<>
<Tabs.Screen
name="profile"
options={{
href: "/profile",
headerShown: false,
headerTitle: "Profile",
lazy: false,
tabBarAccessibilityLabel: translate("tabsNavigator:profileTab"),
tabBarLabel: translate("tabsNavigator:profileTab"),
tabBarIcon: ({ focused }) => (
<MaterialCommunityIcons
name="account-circle"
color={focused ? colors.palette.accent600 : colors.palette.neutral600}
size={tabBarIconSize}
/>
),
}}
/>
<Stack
screenOptions={{
headerShown: true,
}}
/>
</>
)
}