How to Combine and Make Drawer and Bottom Tab Navigators Visible on Every Screen in React Native

How to Combine and Make Drawer and Bottom Tab Navigators Visible on Every Screen in React Native

Introduction

Navigation is one of the important aspects of a mobile app these days as nearly almost all the app we use today either have Drawer Navigator or a Bottom Tab Navigator where we can then move in between the screens of both navigators. Also having different screens on our mobile app is handled by the React Navigation with the help of the Stack Navigator. The addition of these navigatiors to a react native app is acheived by using the React Navigation Library. When building react native apps, we sometimes need to create only one navigator to work with such as for Instagram, only the Bottom Tab Navigator is required to build this app and transition between the screens. However, there are some cases where we are presented with the challenge of combining multiple types of navigator in the app we are building. Say for example, we need to combine drawer and bottom tab and need to make the bottom tab navigator to display even when we click any of the drawer route.

The React Navigation Library does is not designed in such a way that this functionality can be achieved directly.

In this tutorial, we will be using the Bottom Tab, Drawer and Stack Navigators to cover two use cases:

  • A simple use case where we can use the Bottom Tab Navigator in a single Drawer route.
  • A complex flow where the Bottom Tab Bar will be visible and accessible inside our Drawer Routes.

Let's get into it!

Installing Dependencies

In this tutorial, we will be using Expo CLI to set up our development environment by initiating the command:

expo init combineNavs

Select the blank template to get the expo app up and running.

image.png

Now, let's install all the dependencies that we will be needing for this project:

yarn add @react-navigation/native @react-navigation/native-stack @react-navigation/stack
expo install react-native-screens react-native-safe-area-context

For the BottomTab Navigator

yarn add @react-navigation/bottom-tabs

For the Drawer Navigator

yarn add @react-navigation/drawer
expo install react-native-gesture-handler react-native-reanimated

Finally, add this code to the App.js of your app to complete the installation of the react-native-gesture-handler.

NB: This code should be at the top of the App.js file and nothing else should be before it.

According to the official documentation:

If you are building for Android or iOS, do not skip this step, or your app may crash in production even if it works fine in development. This is not applicable to other platforms.

import 'react-native-gesture-handler';

File Structure

When building mobile apps it is important to consider a more readable file structure, so as the code gets complicated it can easily be separated and save refactoring in the process. So before setting up our file structure we need to understand what we want to build and how the flow looks like, this will allow us to have an easy set up. To make the file structure easy, we will have 2 folders: navigator and screens. The navigator will hold everything regarding the Drawer and BottomTab Navigators while the screens folder will contain all the screens for the Drawer and BottomTab Screen. This way we can easily separate our navigators from the screens we are rendering.

image.png

So, as previously mentioned, the navigator folder will hold both the navigator files while the screens used in this files will be imported from the respective folders from the screen folder. Finally the Drawer Navigator is imported to the index.js which is then used inside the App.js file.

Drawer Navigators and Screens

The drawer navigator will contain three (3) different routes created in the DrawerTabScreens: Home Navigator.js, SettingsNavigator.js, PrivacyNavigator.js

The HomeNavigator.js will be created as follows:

import { StyleSheet, Text, View } from "react-native";
import React from "react";
import { createStackNavigator } from "@react-navigation/stack";

const Stack = createStackNavigator();

const Home = () => (
  <View style={styles.container}>
    <Text>Home</Text>
  </View>
);

const HomeNavigator = () => {
  return (
    <Stack.Navigator
      screenOptions={{
        headerShown: false,
      }}
    >
      <Stack.Screen name="HomeScreen" component={Home} />
    </Stack.Navigator>
  );
};

export default HomeNavigator;

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
});

The other 2 Routes (Settings and Privacy) will be created in a similar way with their respective names. You can get the code for the other two screens from my github gist here.

Now that we have defined the screens for our drawer tab, we can create the drawer navigator as follows:

import React from "react";
import { createDrawerNavigator } from "@react-navigation/drawer";
import HomeNavigator from "../screens/DrawerTabScreens/HomeNavigator";
import SettingsNavigator from "../screens/DrawerTabScreens/SettingsNavigator";
import PrivacyNavigator from "../screens/DrawerTabScreens/PrivacyNavigator";

const Drawer = createDrawerNavigator();
const DrawerNavigator = () => {
  return (
    <Drawer.Navigator useLegacyImplementation>
      <Drawer.Screen
        name="HomeNavigator"
        component={HomeNavigator}
        options={{
          title: "Home",
        }}
      />
      <Drawer.Screen
        name="SettingsNavigator"
        component={SettingsNavigator}
        options={{
          title: "Settings",
        }}
      />
      <Drawer.Screen
        name="PrivacyNavigator"
        component={PrivacyNavigator}
        options={{
          title: "Privacy",
        }}
      />
    </Drawer.Navigator>
  );
};
export default DrawerNavigator;

Now let's check what we have in our Simulator.

drawer.gif

BottomTab Navigators and Screens

Now that we have implemented our Drawer Navigator routes, it's time to implement the bottom tab navigator. Our BottomTabNavigator will have 4 different screens : HomeNavigator, ShopNavigator, PaymentNavigator, ExploreNavigator. We have created the Home Navigator previously, so we will proceed to create the ShopNavigator as follow:

import { StyleSheet, Text, View } from "react-native";
import React from "react";
import { createStackNavigator } from "@react-navigation/stack";

const Stack = createStackNavigator();

const Shop = () => (
  <View style={styles.container}>
    <Text>Shop</Text>
  </View>
);

const ShopNavigator = () => {
  return (
    <Stack.Navigator
      screenOptions={{
        headerShown: false,
      }}
    >
      <Stack.Screen name="Shop" component={Shop} />
    </Stack.Navigator>
  );
};

export default ShopNavigator;

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
});

The other two screens (ExploreNavigator and PaymentNavigator) can be found in my gist here.

Now, we can create our BottomTabNavigator:

import React from "react";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import HomeNavigator from "../screens/DrawerTabScreens/HomeNavigator";
import ShopNavigator from "../screens/BottomTabScreens/Shop";
import PaymentNavigator from "../screens/BottomTabScreens/Payment";
import ExploreNavigator from "../screens/BottomTabScreens/Explore";

const Tab = createBottomTabNavigator();

const BottomTabNavigator = () => {
  return (
    <Tab.Navigator
      screenOptions={{
        headerShown: false,
      }}
    >
      <Tab.Screen name="HomeNavigator" component={HomeNavigator} />
      <Tab.Screen name="ShopNavigator" component={ShopNavigator} />
      <Tab.Screen name="ExploreNavigator" component={ExploreNavigator} />
      <Tab.Screen name="PaymentNavigator" component={PaymentNavigator} />
    </Tab.Navigator>
  );
};

export default BottomTabNavigator;

In our BottomTabNavigator, the HomeNavigator is our first screen which we have already included in the DrawerNavigator. In the HomeNavigator there's an Home screen, hence the inital screen route that will be rendered in the bottom tab navigator is the home screen. Since we are combining the navigators and we need to show the home screen when a user is on the home route in the drawer navigation, we can then replace the HomeNavigator component in the drawer navigation with the BottomNavigatior component as a whole.

Let's replace the HomeNavigator component with the BottomTabNavigator component we just created and see what we have:

DrawerNavigation.js

const DrawerNavigator = () => {
  return (
    <Drawer.Navigator useLegacyImplementation>
      <Drawer.Screen
        name="HomeTabs"
        component={BottomTabNavigator}
        options={{
          title: "Home",
        }}
      />
      <Drawer.Screen
        name="SettingsNavigator"
        component={SettingsNavigator}
        options={{
          title: "Settings",
        }}
      />
      <Drawer.Screen
        name="PrivacyNavigator"
        component={PrivacyNavigator}
        options={{
          title: "Privacy",
        }}
      />
    </Drawer.Navigator>
  );
};
export default DrawerNavigator;

bottomtab.gif

Now we have implemented both the DrawerTab and the BottomTab Navigators, it's time to add icons and some other styles to our BottomTab Navigator and it's screens.

BottomTab Navigator.js

const BottomTabNavigator = () => {
  return (
    <Tab.Navigator
      screenOptions={{
        headerShown: false,
        tabBarStyle: {
          backgroundColor: "lavender",
          paddingTop: 5,
        },
      }}
    >
      <Tab.Screen
        name="HomeNavigator"
        component={HomeNavigator}
        options={{
          tabBarLabel: ({ focused }) => (
            <Text style={{ color: focused ? "black" : "#7F8487" }}>Home</Text>
          ),
          tabBarIcon: ({ size, focused }) => (
            <Ionicons
              name="ios-home"
              size={size}
              color={focused ? "black" : "#7F8487"}
            />
          ),
        }}
      />
      <Tab.Screen
        name="ShopNavigator"
        component={ShopNavigator}
        options={{
          tabBarLabel: ({ focused }) => (
            <Text style={{ color: focused ? "black" : "#7F8487" }}>Shop</Text>
          ),
          tabBarIcon: ({ size, focused }) => (
            <MaterialCommunityIcons
              name="shopping-outline"
              size={size}
              color={focused ? "black" : "#7F8487"}
            />
          ),
        }}
      />
      <Tab.Screen
        name="ExploreNavigator"
        component={ExploreNavigator}
        options={{
          tabBarLabel: ({ focused }) => (
            <Text style={{ color: focused ? "black" : "#7F8487" }}>
              Explore
            </Text>
          ),
          tabBarIcon: ({ size, focused }) => (
            <MaterialIcons
              name="explore"
              size={size}
              color={focused ? "black" : "#7F8487"}
            />
          ),
        }}
      />
      <Tab.Screen
        name="PaymentNavigator"
        component={PaymentNavigator}
        options={{
          tabBarLabel: ({ focused }) => (
            <Text style={{ color: focused ? "black" : "#7F8487" }}>
              Payment
            </Text>
          ),
          tabBarIcon: ({ size, focused }) => (
            <MaterialIcons
              name="payment"
              size={size}
              color={focused ? "black" : "#7F8487"}
            />
          ),
        }}
      />
    </Tab.Navigator>
  );
};

The icons used were gotten from expo-vector-icons.

screenOptions - "Options to configure how the screens inside the group get presented in the navigator. It accepts either an object or a function returning an object".

tabBarLabel - "Title string of a tab displayed in the tab bar or a function that given { focused: boolean, color: string } returns a React.Node, to display in tab bar." In our case it returns the 'home, shop, explore, payment' in our bottomtab nav.

The tabBarIcon receives the focused and size property. The focus property can be used to highlight the current route while the size can be used to set the icon size.

You can read up on other props for the tab navigator and screens on the official docs.

Now, let's see what we have:

icons.gif

Combining the Navigators

So a recap of all we have implemented: we started by implementing the drawer routes for the drawer navigator, then proceed to implement the drawer navigator itself, then we implemented the bottom tab navigator and combine the bottom tab nav with the drawer navigation. Then we added styles and icons to our bottom navigation bar. But one thing to notice is that the bottom tab bar disappears once we click on the settings screen.

Why? When working with navigations in react-native, the navigation interface of the child navigator only appears in the screens which it contains. Meaning, the BottomTab nav will only show when we are on the home route only and not in any other routes. So the only ideal way to solve this issue is for the BottomTab Navigator to contain all the screens (including the screens for the drawer navigator).

The BottomTab Navigator will house all the screens for the application, therefore the only screen that will be left in the DrawerNavigator.js is the BottomTab Navigator while we will still render the SettingsNavigator and PrivacyNavigator.

We will create a new ScreenRoutes file where we will create a configuration object for each of our screens which we will then store in a routes array.

ScreenRoutes

import { Ionicons } from "@expo/vector-icons";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { MaterialIcons } from "@expo/vector-icons";

export const screens = {
  HomeTabs: "HomeTabs",
  Home: "Home",
  HomeNavigator: "HomeNavigator",
  Shop: "Shop",
  ShopNavigator: "ShopNavigator",
  Explore: "Explore",
  ExploreNavigator: "ExploreNavigator",
  Payment: "Payment",
  PaymentNavigator: "PaymentNavigator",
  Settings: "Settings",
  SettingsNavigator: "SettingsNavigator",
  Privacy: "Privacy",
  PrivacyNavigator: "PrivacyNavigator",
};

export const routes = [
  {
    name: screens.HomeTabs,
    focusedRoute: screens.HomeTabs,
    title: "Home",
    showInTab: false,
    showInDrawer: false,
    icon: (focused) => (
      <Ionicons
        name="ios-home"
        size={24}
        color={focused ? "black" : "#7F8487"}
      />
    ),
  },
  {
    name: screens.HomeNavigator,
    focusedRoute: screens.HomeNavigator,
    title: "Home",
    showInTab: true,
    showInDrawer: true,
    icon: (focused) => (
      <Ionicons
        name="ios-home"
        size={24}
        color={focused ? "black" : "#7F8487"}
      />
    ),
  },
  {
    name: screens.Home,
    focusedRoute: screens.HomeNav,
    title: "Home",
    showInTab: true,
    showInDrawer: false,
    icon: (focused) => (
      <Ionicons
        name="ios-home"
        size={24}
        color={focused ? "black" : "#7F8487"}
      />
    ),
  },
  {
    name: screens.Shop,
    focusedRoute: screens.ShopNavigator,
    title: "Shop",
    showInTab: true,
    showInDrawer: false,
    icon: (focused) => (
      <MaterialCommunityIcons
        name="shopping-outline"
        size={24}
        color={focused ? "black" : "#7F8487"}
      />
    ),
  },
  {
    name: screens.ShopNavigator,
    focusedRoute: screens.ShopNavigator,
    title: "Shop",
    showInTab: true,
    showInDrawer: false,
    icon: (focused) => (
      <MaterialCommunityIcons
        name="shopping-outline"
        size={24}
        color={focused ? "black" : "#7F8487"}
      />
    ),
  },
  {
    name: screens.Explore,
    focusedRoute: screens.ExploreNavigator,
    title: "Explore",
    showInTab: true,
    showInDrawer: false,
    icon: (focused) => (
      <MaterialIcons
        name="explore"
        size={24}
        color={focused ? "black" : "#7F8487"}
      />
    ),
  },
  {
    name: screens.ExploreNavigator,
    focusedRoute: screens.ExploreNavigator,
    title: "Explore",
    showInTab: true,
    showInDrawer: false,
    icon: (focused) => (
      <MaterialIcons
        name="explore"
        size={24}
        color={focused ? "black" : "#7F8487"}
      />
    ),
  },
  {
    name: screens.Payment,
    focusedRoute: screens.PaymentNavigator,
    title: "Payment",
    showInTab: true,
    showInDrawer: false,
    icon: (focused) => (
      <MaterialIcons
        name="payment"
        size={24}
        color={focused ? "black" : "#7F8487"}
      />
    ),
  },
  {
    name: screens.PaymentNavigator,
    focusedRoute: screens.PaymentNavigator,
    title: "Payment",
    showInTab: true,
    showInDrawer: false,
    icon: (focused) => (
      <MaterialIcons
        name="payment"
        size={24}
        color={focused ? "black" : "#7F8487"}
      />
    ),
  },
  {
    name: screens.Settings,
    focusedRoute: screens.SettingsNavigator,
    title: "Settings",
    showInTab: false,
    showInDrawer: false,
  },
  {
    name: screens.SettingsNavigator,
    focusedRoute: screens.SettingsNavigator,
    title: "Settings",
    showInTab: false,
    showInDrawer: true,
  },
  {
    name: screens.Privacy,
    focusedRoute: screens.PrivacyNavigator,
    title: "Privacy",
    showInTab: false,
    showInDrawer: false,
  },
  {
    name: screens.PrivacyNavigator,
    focusedRoute: screens.PrivacyNavigator,
    title: "Privacy",
    showInTab: false,
    showInDrawer: true,
  },
];

Now let's use these screens and routes in our BottomTabNavigator.js file

We will introduce the other two routes present in the drawe screen (Settings and Profile) as earlier mentioned and create a bottomTabOptions function which will render only the screens we have declared to be true in showInTab.

import React from "react";
import { Text, View } from "react-native";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import HomeNavigator from "../screens/DrawerTabScreens/HomeNavigator";
import ShopNavigator from "../screens/BottomTabScreens/Shop";
import PaymentNavigator from "../screens/BottomTabScreens/Payment";
import ExploreNavigator from "../screens/BottomTabScreens/Explore";
import SettingsNavigator from "../screens/DrawerTabScreens/SettingsNavigator";
import PrivacyNavigator from "../screens/DrawerTabScreens/PrivacyNavigator";
import { routes, screens } from "./ScreenRoutes";

const Tab = createBottomTabNavigator();

const bottomTabOptions = ({ route }) => {
// getting the route object
  const item = routes.find((routeItem) => routeItem.name === route.name);


  if (!item.showInTab) {
    return {
      tabBarButton: () => <View style={{ width: 0 }} />, // if item.showInTab is false then we are using this view to hide it.
      headerShown: false,
      tabBarStyle: {
        backgroundColor: "lavender",
        paddingTop: 5,
      },
      title: item.title,
    };
  }

  return {
    title: item.title,
    headerShown: false,
    tabBarIcon: ({ focused }) => item.icon(focused),
  };
};

const BottomTabNavigator = () => {
  return (
    <Tab.Navigator screenOptions={bottomTabOptions}>
      <Tab.Screen
        name={screens.HomeNavigator}
        component={HomeNavigator}
      />
      <Tab.Screen
        name={screens.ShopNavigator}
        component={ShopNavigator}
      />
      <Tab.Screen
        name={screens.ExploreNavigator}
        component={ExploreNavigator}
      />
      <Tab.Screen
        name={screens.PaymentNavigator}
        component={PaymentNavigator}


      {/* drawer routes */}
      <Tab.Screen
        name={screens.SettingsNavigator}
        component={SettingsNavigator}
      />
      <Tab.Screen
        name={screens.PrivacyNavigator}
        component={PrivacyNavigator}
      />
    </Tab.Navigator>
  );
};

export default BottomTabNavigator;

We are using the !item.showInTab to check for the falsy value from what we have declared in the routes of the ScreenRoutes.js file. Notice how we are now rendering the name of our Tab.Screen dynamically.

Now we make the final changes in the DrawerNavigator.js file.

Here we will be making use of the CustomDrawerContent to render the content of our drawer navigator.

drawerContent​ is a Function that returns React element to render as the content of the drawer, for example, navigation items

You can learn more about the custom drawer component from the docs here

import React from "react";
import { Text } from "react-native";
import {
  createDrawerNavigator,
  DrawerContentScrollView,
  DrawerItem,
} from "@react-navigation/drawer";
import BottomTabNavigator from "./BottomTabNavigator";
import { routes, screens } from "./ScreenRoutes";

const Drawer = createDrawerNavigator();

const CustomDrawerContent = (props) => {
  return (
    <DrawerContentScrollView {...props}>
      {routes
        .filter((route) => route.showInDrawer)
        .map((route, index) => {
          return (
            <DrawerItem
              key={index}
              label={() => (
                <Text style={{ fontSize: 18, fontWeight: "bold" }}>
                  {route.title}
                </Text>
              )}
              onPress={() => props.navigation.navigate(route.name)}
            />
          );
        })}
    </DrawerContentScrollView>
  );
};
const DrawerNavigator = () => {
  return (
    <Drawer.Navigator
      useLegacyImplementation
      screenOptions={{
        drawerType: "front",
      }}
      drawerContent={(props) => <CustomDrawerContent {...props} />}
    >
      <Drawer.Screen
        name={screens.HomeTabs}
        component={BottomTabNavigator}
        options={{
          title: "Home",
        }}
      />
    </Drawer.Navigator>
  );
};
export default DrawerNavigator;

The other two screens present in our Drawer.Navigator have been removed and we are rendering the content of our drawer using the CustomDrawerContent function where we are filtering the routes that has a truthy value of showInDrawer and then looping through the returned array to render the contents.

Now let's see the end result of what we have:

combine.gif

Conclusion

So, we have been able to implement a navigation whereby the bottom and drawer tab are visible on every screen/route and we have also make our screens and route dynamic. You can further make your own customization of this code to suit your preference.

You can find the complete code here on my github repo

Function that returns React element to render as the content of the drawer, for example, navigation items

Debugging

  • Reanimated 2 failed to create a worklet, maybe you forgot to add Reanimated's babel plugin?

image.png

This is an error I first encountered due to the installation of react-native-gesture-handler and react-native-reanimated. This is needed for the drawer navigation as was mentioned on the official documentation.

Solution You need to update your babel.config.js with this plugin:

plugins: [
    "react-native-reanimated/plugin",
  ],

Then stop your server and clear the cache using the command expo r -c and the error will be resolved.

  • If you check our drawer slider, you will see that when we click the menu button to slide the drawer it does not slide properly. To fix this you can attach this code to the screen options of the DrawerNavigator:
 <Drawer.Navigator
      useLegacyImplementation
      screenOptions={{
        drawerType: "front",
      }}
/>