expoexpo-router

Expo Router: Achieve a back navigation from nested Native Stack under Tab Stack when navigating to parent router


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?


Solution

  • 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,
              }}
            />
        </>
      )
    }