Skip to content

Nesting Tab and Stack navigators in React Native and Expo apps

Published:

7 min read

Using react-navigation you can definitely nest different types of navigators. The term nesting navigators mean that rendering one navigator inside a screen of another navigator.

The possible scenarios of nesting navigators are:

In this tutorial, let us examine one of the above scenarios by nesting Tab inside a stack navigator. Whether you are following from the previous tutorial on building a stack navigator using a component-based configuration with the latest version of the react-navigation library, or not, here is the source code of the Expo demo app that is going to be leveraged. This demo app, already has a stack navigator running. You can download the source code from the Github rep here.

Table of contents

Open Table of contents

Requirements

Requirements for this tutorial is simple. Have the following installed on your local dev environment.

Do note that, without dwelling much into the configuration of native binaries with the react-navigation library, I am going to use a project that is already generated using expo-cli. If you wish to start afresh, choose the blank template.

Install dependencies

Install the following dependency to setup a Tab Navigator. Run the following command from a terminal window.

yarn add @react-navigation/bottom-tabs

This package is going to allow the app to have a simple tab bar appear at the bottom of the screen and switch between different routes. The demo app we are going to build is going to consist of two tabs. We are going to nest the stack navigator inside the first tab and create a mock screen for the second tab.

Create a mock screen

Even though the current app structure has three different screen components (open src/screens to view them), let us create another screen component called Profile that will act as the second tab. Create a new file called src/screens/Profile.js with the following code snippet:

import React from "react";
import { StyleSheet, View, Text } from "react-native";

function Profile(props) {
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Profile Tab</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "#ebebeb",
  },
  text: {
    color: "#101010",
    fontSize: 24,
    fontWeight: "bold",
  },
});

export default Profile;

Create a tab navigator

In this section, let us set up a basic Tab navigator. Start by renaming the file MainStackNavigator to AppNavigator.js in the directory src/navigation.

After the renaming, the routes config file, after other import statements, import the createBottomTabNavigator from @react-navigation/bottom-tabs as well as the Profile screen component.

import * as React from "react";
import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";
// add this after other import statements
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";

import Home from "../screens/Home";
import Detail from "../screens/Detail";
import Settings from "../screens/Settings";
// add this after other import statements
import Profile from "../screens/Profile";

Then create an instance of the createBottomTabNavigator called Tab as below:

// after other instances
const Tab = createBottomTabNavigator();

Next, create a function called MainTabNavigator(). Using Tab.Navigator you can define the structure of the routes and use Tab.Screen you can define each of the routes.

Let us define tab routes for now: Home and Profile.

function MainTabNavigator() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="Home" component={Home} />
      <Tab.Screen name="Profile" component={Profile} />
    </Tab.Navigator>
  );
}

Now, in the MainStackNavigator() instead of passing the Home screen, let us pass the MainTabNavigator.

<Stack.Screen name="Home" component={MainTabNavigator} />

Lastly, to make all of this work, open App.js file in the root of the project and modify the statement that imports the MainStackNavigator with the correct file name.

import React from "react";

// make sure this matches the file name of navigator config
import MainStackNavigator from "./src/navigation/AppNavigator";

export default function App() {
  return <MainStackNavigator />;
}

Go back to the terminal window, execute expo start and open up an Expo client inside a simulator or a real device. You are going to get the following result.

Adding icon and changing active tint color

From the last image, you notice that the active tab is highlighted by a tint color of blue and the non-active tab is of gray. Let us change this tint color.

Open AppNavigator.js file at Tab.Navigator add a prop called tabBarOptions. This prop allows you to customize the tab bar shared between different routes.

Add the following:

<Tab.Navigator
  tabBarOptions={{
    activeTintColor: "#101010",
  }}
>
  {/* rest remains same */}
</Tab.Navigator>

Go to the simulator device, you are going to notice that the active tab bar label has a color of black from the previous blue.

Let us add some icons to the tab bar. Start by importing the Ionicons from @expo/vector-icons.

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

Then, in each Tab.Screen, add an options prop that is going to have a property of tabBarIcon. This function returns the component Ionicons. Pass the arguments color and size you can maintain the active tint color.

<Tab.Navigator
  tabBarOptions={{
    activeTintColor: "#101010",
  }}
>
  <Tab.Screen
    name="Home"
    component={Home}
    options={{
      tabBarIcon: ({ color, size }) => (
        <Ionicons name="ios-home" color={color} size={size} />
      ),
    }}
  />
  <Tab.Screen
    name="Profile"
    component={Profile}
    options={{
      tabBarIcon: ({ color, size }) => (
        <Ionicons name="ios-person" size={size} color={color} />
      ),
    }}
  />
</Tab.Navigator>

Here is the output:

You can even change the background of the tab bar by adding a style property to tabBarOptions.

<Tab.Navigator
  tabBarOptions={{
    activeTintColor: "#101010",
    style: {
      backgroundColor: "#ffd700",
    },
  }}
>
  {/* rest remains same */}
</Tab.Navigator>

Here is the output for the above snippet:

Passing screenOptions in a Tab Navigator

The previous section is one way to add icons to each route or screen in the tab bar. There is another way you can do it by passing screenOptions in the wrapper Tab.Navigator. This prop is used to modify or add common styles to a navigator.

function MainTabNavigator() {
  return (
    <Tab.Navigator
      tabBarOptions={{
        activeTintColor: "#101010",
        style: {
          backgroundColor: "#ffd700",
        },
      }}
      screenOptions={({ route }) => ({
        tabBarIcon: ({ color, size }) => {
          let iconName;
          if (route.name == "Home") {
            iconName = "ios-home";
          } else if (route.name == "Profile") {
            iconName = "ios-person";
          }
          return <Ionicons name={iconName} color={color} size={size} />;
        },
      })}
    >
      <Tab.Screen name="Home" component={Home} />
      <Tab.Screen name="Profile" component={Profile} />
    </Tab.Navigator>
  );
}

There is no change in the functioning of the tab navigator from the previous section, as you can notice below:

Updating the header title for the nested child navigator

Right now the title for each tab screen is going to be the same. This is because the root navigator (which, here is the Stack Navigator) structure is going to look at its immediate children, which are Home, Detail and Settings screen. In the current scenario, if you are to set the title for the Profile screen passing the prop options, it is not going to work.

This is because the Profile screen is a child of the Tab Navigator and not Stack Navigator. The tab navigator is nested inside the Stack navigator and thus, Profile is not the immediate child to Stack Navigator.

For each tab to have its own title (since the tab navigator is nested inside the stack navigator), you have to determine the title for a specific tab screen based on the navigation state from the property route.state.

This can be done by defining a helper function called getHeaderTitle that has route as its parameter. Why pass route? Because it contains the state property which refers to the child’s navigator state and the value of the currently active route name can be obtained from this state.

Add a function called getHeaderTitle in AppNavigator.js file.

function getHeaderTitle(route) {
  const routeName = route.state
    ? route.state.routes[route.state.index].name
    : route.params?.screen || "Home";

  switch (routeName) {
    case "Home":
      return "Home";
    case "Profile":
      return "Profile";
  }
}

Then, as per the recommended way, add the options prop to the Stack.Screen route whose value is Home.

<Stack.Screen
  name="Home"
  component={MainTabNavigator}
  options={({ route }) => ({
    headerTitle: getHeaderTitle(route),
  })}
/>

Now, when visiting the Profile tab, you are going to get the desired title in the header.

Conclusion

Congratulations! You’ve completed this tutorial.

In this tutorial, we discuss only one scenario of nesting navigators. The main objective here is to get familiar with the component-based configuration of the Tab Navigator in the latest version of the react-navigation library.

Here is the link to the complete Tab Navigator API here I’d recommend you to check.

You can find the complete code for this tutorial at this GitHub repo.

Originally published at Heartbeat.fritz.ai

I'm a software developer and a technical writer. On this blog, I write about my learnings in software development and technical writing.

Currently, working as a documentation lead at Expo.