Originally published at https://blog.deversity.com/2021/10/combining-drawer-tab-and-stack.html
Today we will be making use of the Drawer, Tab and Stack navigators. We will cover two cases:
- a simpler scenario where we utilize the Tab Navigator in a single Drawer route
- a more complicated flow where we want the Tab bar to be visible and accessible inside all our Drawer routes.
In this second example, we will try to overcome a design restriction of React Navigation - the different Navigators, if used together, can only be nested inside one another, and therefore can't be intertwined.
Introduction
Adding navigation to a React Native application is greatly helped by using React Navigation library. It provides different types of navigators, with plenty of customization power. In some simple cases we can get by with using just one navigator, but often times we are presented with a challenge to combine multiple types in an app.
The example chosen is to build an app for a hotel chain. Some of the features include booking a room at one of the hotels, browsing the different locations and using reward points. Here is a preview of what we will be building:
We can see right away the use of Drawer and Tab navigators. We will also implement each of the routes as a Stack Navigator, since we now that, for instance, the Book flow will contain multiple screens.
Getting started
(if this is your first React Native project, please read the official getting started guide before continuing)
Let's initialise a new project. In your terminal, navigate to an empty directory and run the following command:
$ npx react-native init NavigationDemo --version 0.64.2
The react version installed at the time of writing was 17.0.2, while the react-native version was 0.64.2.
Next, let's install react navigation and its dependencies:
$ npm install @react-navigation/native react-native-screens react-native-safe-area-context react-native-gesture-handler react-native-reanimated @react-navigation/stack @react-navigation/drawer @react-navigation/bottom-tabs
If you're developing for IOS, you also need to install the pods:
$ cd ios; npx pod install; cd ..
Replace the contents of your App.js
file with the following code:
import React from 'react'
import { SafeAreaView, View, StatusBar, StyleSheet, Text } from 'react-native'
const App = () => {
return (
<SafeAreaView style={styles.safeArea}>
<StatusBar barStyle="dark-content" />
<View>
<Text>Hello navigation!</Text>
</View>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
overflow: 'hidden',
},
})
export default App
Stack and Drawer navigators
Now we can go about adding the different navigators to our app. Remember, for this first example we want the DrawerNavigator to be the main (always visible) navigator in our app, with the BottomTabNavigator visible if the Home route is focused in the Drawer. Letβs begin by adding the following file structure in our project (all the files remain empty for now):
You can download the hotel_logo
from the github repo provided at the end of this tutorial, or use your own. Next, we will create our Drawer Navigator that contains three routes (our Stack Navigators). For now, the stacks will contain a single screen defined directly in the stack file. In a real app, the stack can contain many screens, but it's important to have at least one. Following are the contents of the stack files:
HomeStackNavigator.js:
import React from 'react'
import { View, Text } from 'react-native'
import { createStackNavigator } from '@react-navigation/stack'
const Stack = createStackNavigator()
const Home = () => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Home screen!</Text>
</View>
)
const HomeStackNavigator = () => {
return (
<Stack.Navigator screenOptions={{
headerShown: false,
}}>
<Stack.Screen name="Home" component={Home} />
</Stack.Navigator>
)
}
export default HomeStackNavigator
MyRewardsStackNavigator.js:
import React from 'react'
import { View, Text } from 'react-native'
import { createStackNavigator } from '@react-navigation/stack'
const Stack = createStackNavigator()
const MyRewards = () => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>MyRewards screen!</Text>
</View>
)
const MyRewardsStackNavigator = () => {
return (
<Stack.Navigator screenOptions={{
headerShown: false,
}}>
<Stack.Screen name="MyRewards" component={MyRewards} />
</Stack.Navigator>
)
}
export default MyRewardsStackNavigator
LocationsStackNavigator.js:
import React from 'react'
import { View, Text } from 'react-native'
import { createStackNavigator } from '@react-navigation/stack'
const Stack = createStackNavigator()
const Locations = () => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Locations screen!</Text>
</View>
)
const LocationsStackNavigator = () => {
return (
<Stack.Navigator screenOptions={{
headerShown: false,
}}>
<Stack.Screen name="Locations" component={Locations} />
</Stack.Navigator>
)
}
export default LocationsStackNavigator
We will explain screenOptions in a moment. Now that we have defined our drawer stack navigators, we can create the DrawerNavigator:
DrawerNavigator.js:
import * as React from 'react'
import { createDrawerNavigator } from '@react-navigation/drawer'
import HomeStackNavigator from './stack-navigators/HomeStackNavigator'
import MyRewardsStackNavigator from './stack-navigators/MyRewardsStackNavigator'
import LocationsStackNavigator from './stack-navigators/LocationsStackNavigator'
const Drawer = createDrawerNavigator()
const DrawerNavigator = () => {
return (
<Drawer.Navigator>
<Drawer.Screen name="HomeStack" component={HomeStackNavigator} />
<Drawer.Screen name="MyRewardsStack" component={MyRewardsStackNavigator} />
<Drawer.Screen name="LocationsStack" component={LocationsStackNavigator} />
</Drawer.Navigator>
)
}
export default DrawerNavigator
And add it to our NavigationContainer in App.js
...
import { NavigationContainer } from '@react-navigation/native'
import DrawerNavigator from './src/navigation/DrawerNavigator'
const App = () => {
return (
<SafeAreaView style={styles.safeArea}>
<StatusBar barStyle="dark-content" />
<NavigationContainer>
<DrawerNavigator />
</NavigationContainer>
</SafeAreaView>
)
}
...
Let's run our code to see the results so far. Run
$ npx react-native start
to start the Metro bundler. Then, in a separate terminal, run
$ npx react-native run-android
or
$ npx react-native run-ios
depending on which platform you're developing for (run both one after the other if you want to simultaneously work on both platforms).
We can see the result now. We have React Navigation's default header, an icon to open the drawer, and our stacks in the drawer menu. We can navigate freely between those stacks.
Now let's circle back to the screenOptions
we defined in the stack navigators. Try setting headerShown: true
in HomeStackNavigator
and observe what happens:
The Home component's header is rendered below the Drawer Navigator's. This is because the parent navigator's UI is rendered on top of child navigator. Since we obviously want only one header, specifying headerShown: false
for each of the stack navigator's screenOptions
hides the default stack header. Note that the title displayed in the drawer header is HomeStack
, not Home
. If we were to navigate to another screen in HomeStack, the title would not change. Could we have kept the Stack header and hidden the Drawer header? Yes! But for now, we want the default Drawer header as it provides us with an easy way to open the drawer - by pressing the menu icon in the header.
Tab navigator
We have added Drawer navigation to our app, and defined stack navigators with screens to add to our drawer menu. Now we need to add tab navigation to our Home Route. Firstly, lets define Book and Contact stack navigators in the same way as before:
BookStackNavigator.js:
import React from 'react'
import { View, Text } from 'react-native'
import { createStackNavigator } from '@react-navigation/stack'
const Stack = createStackNavigator()
const Book = () => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Book screen!</Text>
</View>
)
const BookStackNavigator = () => {
return (
<Stack.Navigator screenOptions={{
headerShown: false,
}}>
<Stack.Screen name="Book" component={Book} />
</Stack.Navigator>
)
}
export default BookStackNavigator
ContactStackNavigator.js:
import React from 'react'
import { View, Text } from 'react-native'
import { createStackNavigator } from '@react-navigation/stack'
const Stack = createStackNavigator()
const Contact = () => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Contact screen!</Text>
</View>
)
const ContactStackNavigator = () => {
return (
<Stack.Navigator screenOptions={{
headerShown: false,
}}>
<Stack.Screen name="Contact" component={Contact} />
</Stack.Navigator>
)
}
export default ContactStackNavigator
Now let's create our Tab Navigator.
BottomTabNavigator
import * as React from 'react'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import HomeStackNavigator from './stack-navigators/HomeStackNavigator'
import BookStackNavigator from './stack-navigators/BookStackNavigator'
import ContactStackNavigator from './stack-navigators/ContactStackNavigator'
const Tab = createBottomTabNavigator()
const BottomTabNavigator = () => {
return (
<Tab.Navigator screenOptions={{
headerShown: false,
}}>
<Tab.Screen name="HomeStack" component={HomeStackNavigator} />
<Tab.Screen name="BookStack" component={BookStackNavigator} />
<Tab.Screen name="ContactStack" component={ContactStackNavigator} />
</Tab.Navigator>
)
}
export default BottomTabNavigator
Notice how the first tab screen we added is the HomeStack, which we have already added in DrawerNavigator. In fact, you can think of BottomTabNavigator as a container of stacks, with the initial stack being HomeStack. Since in HomeStack we have a Home screen, the initial screen being rendered in the Tab navigator is the Home screen. And because we want to show this when the user is on the Home route in the drawer navigation, we will simply replace the HomeStackNavigator component in DrawerNavigator with BottomTabNavigator:
DrawerNavigator.js:
...
import BottomTabNavigator from './BottomTabNavigator'
const Drawer = createDrawerNavigator()
const DrawerNavigator = () => {
return (
<Drawer.Navigator>
<Drawer.Screen name="HomeTabs" component={BottomTabNavigator} />
<Drawer.Screen name="MyRewardsStack" component={MyRewardsStackNavigator} />
<Drawer.Screen name="LocationsStack" component={LocationsStackNavigator} />
</Drawer.Navigator>
)
}
...
Let's look at what we get:
When we are in the first route in DrawerNavigator, we can see the bottom tabs and navigate between them. If we move to another route in the Drawer, the tabs are no longer visible (since the tab navigator is just one of the drawer screens). We have again used headerShown: false
to avoid rendering a double header.
Header and Tab design
We have implemented all our stacks, now we want to implement a few common requirements. Firstly, let's add icons to our tabs. For this project we will use the react-native-vector-icons
package to access FontAwesome icons. The full installation guide can be found here. Once the installation process is complete, we can edit our BottomTabNavigator.js
as follows:
import * as React from 'react'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import { Text, StyleSheet } from 'react-native'
import Icon from 'react-native-vector-icons/FontAwesome'
import HomeStackNavigator from './stack-navigators/HomeStackNavigator'
import BookStackNavigator from './stack-navigators/BookStackNavigator'
import ContactStackNavigator from './stack-navigators/ContactStackNavigator'
const Tab = createBottomTabNavigator()
const BottomTabNavigator = () => {
return (
<Tab.Navigator screenOptions={{ headerShown: false }}>
<Tab.Screen name="HomeStack" component={HomeStackNavigator} options={{
tabBarIcon: ({ focused }) => (
<Icon name="home" size={30} color={focused ? '#551E18' : '#000'} />
),
tabBarLabel: () => <Text style={styles.tabBarLabel}>Home</Text>
}}
/>
<Tab.Screen name="BookStack" component={BookStackNavigator} options={{
tabBarIcon: ({ focused }) => (
<Icon name="bed" size={30} color={focused ? '#551E18' : '#000'} />
),
tabBarLabel: () => <Text style={styles.tabBarLabel}>Book Room</Text>
}}
/>
<Tab.Screen name="ContactStack" component={ContactStackNavigator} options={{
tabBarIcon: ({ focused }) => (
<Icon name="phone" size={30} color={focused ? '#551E18' : '#000'} />
),
tabBarLabel: () => <Text style={styles.tabBarLabel}>Contact Us</Text>
}}
/>
</Tab.Navigator>
)
}
const styles = StyleSheet.create({
tabBarLabel: {
color: '#292929',
fontSize: 12,
},
})
export default BottomTabNavigator
For each stack we have specified an icon and a tab label. tabBarIcon
receives the focused
prop, which we can use to highlight the current route (tabBarLabel
can also receive this prop). There are many possibilities with options
and screenOptions
properties, some of which are explored at https://reactnavigation.org/docs/screen-options/.
Let's use screenOptions
in Drawer Navigator to change the header and route names in the drawer menu:
DrawerNavigator.js:
import * as React from 'react'
import { View, StyleSheet, Image, Text, TouchableOpacity } from 'react-native'
import { createDrawerNavigator, DrawerContentScrollView, DrawerItem } from '@react-navigation/drawer'
import Icon from 'react-native-vector-icons/FontAwesome'
import MyRewardsStackNavigator from './stack-navigators/MyRewardsStackNavigator'
import LocationsStackNavigator from './stack-navigators/LocationsStackNavigator'
import BottomTabNavigator from './BottomTabNavigator'
const Drawer = createDrawerNavigator()
const CustomDrawerContent = (props) => {
return (
<DrawerContentScrollView {...props}>
{
Object.entries(props.descriptors).map(([key, descriptor], index) => {
const focused = index === props.state.index
return (
<DrawerItem
key={key}
label={() => (
<Text style={focused ? styles.drawerLabelFocused : styles.drawerLabel}>
{descriptor.options.title}
</Text>
)}
onPress={() => descriptor.navigation.navigate(descriptor.route.name)}
style={[styles.drawerItem, focused ? styles.drawerItemFocused : null]}
/>
)
})
}
</DrawerContentScrollView>
)
}
const DrawerNavigator = () => {
return (
<Drawer.Navigator
screenOptions={({ navigation }) => ({
headerStyle: {
backgroundColor: '#551E18',
height: 50,
},
headerLeft: () => (
<TouchableOpacity onPress={() => navigation.toggleDrawer()} style={styles.headerLeft}>
<Icon name="bars" size={20} color="#fff" />
</TouchableOpacity>
),
})}
drawerContent={(props) => <CustomDrawerContent {...props} />}
>
<Drawer.Screen name="HomeTabs" component={BottomTabNavigator} options={{
title: 'Home',
headerTitle: () => <Image source={require('../assets/hotel_logo.jpg')} />,
headerRight: () => (
<View style={styles.headerRight}>
<Icon name="bell" size={20} color="#fff" />
</View>
),
}}/>
<Drawer.Screen name="MyRewardsStack" component={MyRewardsStackNavigator} options={{
title: 'My Rewards',
headerTitle: () => <Text style={styles.headerTitle}>My Rewards</Text>,
}}/>
<Drawer.Screen name="LocationsStack" component={LocationsStackNavigator} options={{
title: 'Locations',
headerTitle: () => <Text style={styles.headerTitle}>Our Locations</Text>,
}}/>
</Drawer.Navigator>
)
}
const styles = StyleSheet.create({
headerLeft: {
marginLeft: 15,
},
headerTitle: {
color: 'white',
fontSize: 18,
fontWeight: '500',
},
headerRight: {
marginRight: 15,
},
// drawer content
drawerLabel: {
fontSize: 14,
},
drawerLabelFocused: {
fontSize: 14,
color: '#551E18',
fontWeight: '500',
},
drawerItem: {
height: 50,
justifyContent: 'center'
},
drawerItemFocused: {
backgroundColor: '#ba9490',
},
})
export default DrawerNavigator
Let's breakdown all of the changes. First of all, looking at the Drawer Screens, we can change the header of each drawer item separately. You might not want to display a title when the user is in the Tab navigator, but maybe show the company's logo instead. The headerTitle
prop accepts a string as well as a function - giving us a lot of possibilities for customisation. Furthermore, the title shown in the header can be different than the one shown in the drawer menu.
Next, we want to change the look of the header to fit better with our client's brand. We can do this by passing a function to DrawerNavigator's screenOptions
and specifying header style and components. ScreenOptions
also receives the route
prop. We are passing a function to headerLeft
that renders our menu icon, and toggles the drawer - this toggle function is available in the navigation
object.
Finally, let's customise the Drawer menu. We only want to change the route item styles for now, and unfortunately there isn't a simple DrawerNavigation prop that enables us to do this. Instead, we must pass a custom drawerContent function that enables us to render a completely custom component for each item. We are using the passed props to iterate through these items, but we could also render more routes using <DrawerItem>
, or add an image component at the top of <DrawerContentScrollView>
, or any number of other options.
Conclusion
In this tutorial, we have combined Drawer, Tab and Stack navigators to create a simple navigation flow. We have then, through screenOptions
, supplied customisation to get the look and feel we needed. In the next section, we will explore the problem of having both the Drawer and Tab navigations always visible and connected.
Part 2 of this tutorial can be found here
The complete project can be found on github
Top comments (0)