React Native

3.1 Nested Navigation

Module Still Under Development

Building on what you have learned from the last module, it is time to look at how to manage the multi-level navigation requirements common to modern mobile applications. The initial setup is the same as for the Stack Navigator. Make sure that these dependencies are installed in your project.

npm i @react-navigation/native @react-navigation/native-stack
1
npx expo install react-native-screens react-native-safe-area-context
1

# Bottom Tab Navigation

The usage instructions for Tab.Navigator are very similar to the Stack.Navigator. Before you can use the tab navigator, you need to install the NPM module. See the reference for the Bottom Tab Navigator (opens new window)

npm install @react-navigation/bottom-tabs
1

Then import the createBottomTabNavigator function in your App.js module.


 

import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
1
2

Next create a new instance of the Tab object using the imported function. When invoked, it returns on object with two properties containing React components: Screen and Navigator.

const Tab = createBottomTabNavigator();
1

Then in the JSX, replace Stack.Navigator with Tab.Navigator and replace Stack.Screen with Tab.Screen.

export default function App() {
  return (
    <NavigationContainer>
      <Tab.Navigator>
        <Tab.Screen name="Home" component={HomeScreen} />
        <Tab.Screen name="About" component={AboutScreen} />
        <Tab.Screen name="Contact" component={ContactScreen} />
      </Tab.Navigator>
    </NavigationContainer>
  );
}
1
2
3
4
5
6
7
8
9
10
11

# Add icons

It is easy to add icons to your tabs using the included react-native-vector-icons packages that Expo installs by default.

import Ionicons from 'react-native-vector-icons/Ionicons';
1

Then using the options prop on the Tab.Screen component, set the tabBarIcon. To make the icon dynamic, use a function that is given the focused state, color, and size params.




 
 
 
 
 


<Tab.Screen
  name="Contact"
  component={ContactScreen}
  options={{
    tabBarIcon: ({ focused, size, color }) => (
      <Ionicons name={'ios-paper-plane'} size={size} color={color} />
    ),
  }}
/>
1
2
3
4
5
6
7
8
9

You have complete flexibility in styling your tabs to match your designs. For example, let's use indigo as our key brand colour and then set up the tab bar options.

<Tab.Navigator
  initialRouteName="Home"
  screenOptions={{
    tabBaractiveTintColor: 'hsl(275, 100%, 23%)',
    tabBarInactiveTintColor: 'hsl(275, 15%, 60%)',
    tabBarActiveBackgroundColor: 'hsl(120, 100%, 93%)',
    tabBarInactiveBackgroundColor: 'hsl(120, 15%, 60%)',
  }}
>
1
2
3
4
5
6
7
8
9

TIP

Read the docs ... For a full list of options, see the React Navigation API docs (opens new window).

# Nested Navigators

It is pretty common to have a tab navigator as the primary means of navigation in a mobile app. And it is also quite common to have a nested stack navigator in a tab screen to allow the user to view more detailed content or perhaps to edit content.

So, how do you do that?

Because the React Navigation library uses factory functions to create unique instances of the various navigator objects, e.g. Stack or Tab, your application can deploy them in combination. Each will maintain its own independent navigation state.

Lets add a stack navigator to the home screen inside our tab navigator.

# Create a HomeDetailsScreen component

This will be added to the home screen stack navigator in a couple of steps from now.

function HomeDetailsScreen() {
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <Text>Home Details!</Text>
      </View>
    </SafeAreaView>
  );
}
1
2
3
4
5
6
7
8
9

# Install the library

Next you will need to add the native-stack navigator library to your project.

npm install @react-navigation/native-stack
1

And then create a new Stack instance ...

import { createNativeStackNavigator } from '@react-navigation/native-stack';

const Stack = createNativeStackNavigator();
1
2
3

# Create a new HomeStackScreen component

When the "Home" tab is selected in the Tab.Navigator you need to give it the Stack.Navigator to render, which will in turn render either the HomeScreen or the HomeDetailsScreen.

function HomeStackScreen() {
  return (
    <HomeStack.Navigator>
      <HomeStack.Screen name="Home" component={HomeScreen} />
      <HomeStack.Screen name="HomeDetails" component={HomeDetailsScreen} />
    </HomeStack.Navigator>
  );
}
1
2
3
4
5
6
7
8

TIP

Stack state is maintained Notice that when you change tabs and then go back to a tab with a nested Stack.Navigator, the previous state of that stack is unchanged. i.e. it is still showing the same screen as before you switched tabs.

# Passing Props to Screens

It is worth noting that if you want to pass additional props to your Screen elements inside the Stack or Tab Navigators then you need to change how you write the element.

Here is an example using the Tab Navigator. The <Tab.Navigator> uses the function for screenOptions to set the icon on each tab.

The two <Tab.Screen> components are written different. The Home tab simply uses the component={HomeScreen} prop to load that component. However, the List tab needs to pass an array of data to the ListScreen component. We accomplish this by adding a function between the opening and closing tags. The navigation props get passed to this function and we can write those to ListScreen with {...props}. Then we can add our additional props. In this case we create a prop called people and add our state variable as the value for that prop.

<NavigationContainer>
  <Tab.Navigator
    screenOptions={({ route }) => ({
      tabBarIcon: ({ focused, color, size }) => {
        let iconName;
        if (route.name === 'Home') {
          iconName = focused ? 'home' : 'home-outline';
        } else {
          iconName = focused ? 'list-circle' : 'list-circle-outline';
        }
        return <Ionicons name={iconName} size={size} color={color} />;
      },
    })}
  >
    <Tab.Screen name="Home" component={HomeScreen} />

    <Tab.Screen name="List" component={(props) => <ListScreen {...props} people={people} />} />
  </Tab.Navigator>
</NavigationContainer>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# Focus Aware StatusBar

When your app design calls for different background colours for the header on difference screens, the normal behaviour of the StatusBar component isn't helpful. It will keep the settings (light or dark) of the last component to render.

The solution is to create your own FocusAwareStatusBar, using the useIsFocused hook from React Navigation. This will cause your status bar to re-render when the screen is in focus, thereby updating the StatusBar with the correct settings.

import { StatusBar } from 'expo-status-bar';
import { useIsFocused } from '@react-navigation/native';

function FocusAwareStatusBar(props) {
  const isFocused = useIsFocused();
  return isFocused ? <StatusBar {...props} /> : null;
}

export default FocusAwareStatusBar;
1
2
3
4
5
6
7
8
9

You may now use this component as needed in your screen component JSX, with the same props as the regular Expo StatusBar.

# Refresh Control

When you want your user to be able to drag down from the top of the screen with their finger and have that gesture trigger code that refreshes some of your content, use the RefreshControl.

RefreshControl built-in RN component (opens new window)

import { useState, useCallback } from 'react';
import { RefreshControl, SafeAreaView, ScrollView, StyleSheet, Text } from 'react-native';

export default () => {
  const [refreshing, setRefreshing] = useState(false);

  const onRefresh = useCallback(() => {
    setRefreshing(true);
    //refreshing used in <ScrollView> refreshControl attr
    //which loads the <RefreshControl> with the refreshing attr
    setTimeout(() => {
      setRefreshing(false);
    }, 2000);
  }, []);

  return (
    <SafeAreaView style={styles.container}>
      <ScrollView contentContainerStyle={styles.scrollView} refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}>
        <Text>Pull down to see RefreshControl indicator</Text>
      </ScrollView>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  scrollView: {
    flex: 1,
    backgroundColor: 'pink',
    alignItems: 'center',
    justifyContent: 'center',
  },
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# Drawer Navigation

Install the drawer module along with the other navigation requirements, like before. If we are going to add Tab navigation as well, we should install the @react-navigation/bottom-tabs module too.

npm install @react-navigation/native @react-navigation/native-stack
npm install @react-navigation/bottom-tabs
npm install @react-navigation/drawer

npx expo install react-native-screens react-native-safe-area-context
npx expo install react-native-gesture-handler
npx expo install react-native-reanimated
1
2
3
4
5
6
7

The Drawer Navigator also needs the gesture handler and reanimated packages.

The Gesture Handler needs to be imported as the first line in your App.js file.

import 'react-native-gesture-handler';
import { NavigationContainer } from '@react-navigation/native';
import { createDrawerNavigator } from '@react-navigation/drawer';
import { SafeAreaProvider } from 'react-native-safe-area-context';

const Drawer = createDrawerNavigator();
1
2
3
4
5
6

Import and run the factory function.

For the Gesture Handler to work properly you will also need to update your babel.config.js file in your project. Add the plugins part to the settings file.





 



module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    plugins: ['react-native-reanimated/plugin'],
  };
};
1
2
3
4
5
6
7

The App.js file sets up the basic top level navigation structure for the App. Each Drawer.Screen component will have an entry in the Drawer that slides out from the side of the screen.

export default function App() {
  return (
    <SafeAreaProvider>
      <NavigationContainer>
        <Drawer.Navigator initialRouteName="Home">
          <Drawer.Screen name="Home" component={HomeScreen} />
          <Drawer.Screen name="Settings" component={SettingsScreen} />
        </Drawer.Navigator>
      </NavigationContainer>
    </SafeAreaProvider>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12

Just like with the Tab navigator you can use the default drawer and the screens will automatically added as items in the list inside the drawer. Optionally, you can create your own custom container by adding a drawerContent prop in the <Drawer.Navigator> and point it to a <CustomDrawerContent> which would be something like the following:

<Drawer.Navigator drawerContent={(props) => <CustomDrawerContent {...props} />}>/** inside the App function .... */</Drawer.Navigator>;

function CustomDrawerContent(props) {
  return (
    <DrawerContentScrollView {...props}>
      <DrawerItemList {...props} />
      <DrawerItem label="Help" onPress={() => Linking.openURL('https://mywebsite.com/help')} />
    </DrawerContentScrollView>
  );
}
1
2
3
4
5
6
7
8
9
10

If you want to have some nested navigation, then inside the HomeScreen or one of the other screen components loaded by the <Drawer.Screen>s you can add either a <Stack.Navigator> with <Stack.Screen>s or a <Tab.Navigator> with <Tab.Screen>s.

You can nest as many of the navigators as you want.

However, remember that the name attributes added to all of your app's <___.Screen> components must have unique values.

Let's say, as an example we had a <Drawer.Navigator> at the top level inside of App.js and one of the <Drawer.Screen> components loads a HomeScreen page.

//HomeScreen.js
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { View, Text, StyleSheet } from 'react-native';
import FontAwesome from '@expo/vector-icons/FontAwesome';
import AnotherScreen from './AnotherScreen';

const Tab = createBottomTabNavigator();

//this TabScreen is what gets imported by the App.js <Drawer.Screen>
export default function TabScreen({ route, navigation }) {
  return (
    <Tab.Navigator initialRouteName="HomeTab">
      <Tab.Screen
        name="HomeTab"
        component={HomeScreen}
        options={{
          headerShown: false, //don't show a second level of header
          tabBarLabelStyle: { fontSize: 20 }, //size of the text in the tabbar
          title: 'Home', //use this instead of 'HomeTab'
          tabBarIcon: ({ color }) => <FontAwesome name="home" size={20} color={color} />,
        }}
      />
      <Tab.Screen
        name="Another"
        component={AnotherScreen}
        options={{
          headerShown: false,
          tabBarLabelStyle: { fontSize: 20 },
          tabBarIcon: ({ color }) => <FontAwesome name="gear" size={20} color={color} />,
        }}
      />
    </Tab.Navigator>
  );
}

function HomeScreen({ route, navigation }) {
  const insets = useSafeAreaInsets();
  //loaded by the Tab.Navigator as the initial Tab
  return (
    <View style={[s.container, { paddingTop: insets.top }]}>
      <Text style={s.txt}>Home Screen</Text>
    </View>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

Here is a full repo example (opens new window) of a Drawer Navigation App that has a nested Tab Navigation.

# Resources

Stack Navigator (opens new window)

Tab Navigator (opens new window)

Drawer Navigator (opens new window)

# GitHub repo

Sample code for the Nested Navigation with a Drawer.

prof3ssorSt3v3/mad9135-nested-nav-demo (opens new window)

# API docs

Last Updated: 10/3/2023, 6:00:20 PM