Navigation Architecture
For our starter, we want to setup examples of all the different navigators that are provided by react-navigation; tabs, stack, and drawer. At the end of this tutorial our app will look like this.
Setup React-Navigation
% yarn add @react-navigation/native % expo install react-native-screens react-native-safe-area-context
Create Screens
For this example we will create three screens; Home, Settings, & Details.
Create Home Screen
// screens/HomeScreen/HomeScreen.tsx import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; export default function HomeScreen(): JSX.Element { return ( <View style={styles.container}> <Text style={styles.title}>Home</Text> </View>); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, title: { fontSize: 50, } });
Create Settings Screen
// screens/SettingsScreen/SettingsScreen.tsx import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; export default function SettingsScreen(): JSX.Element { return ( <View style={styles.container}> <Text style={styles.title}>Settings</Text> </View>); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, title: { fontSize: 50, } });
Create Details Screen
// screens/DetailsScreen/DetailsScreen.tsx import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; export default function DetailsScreen(): JSX.Element { return ( <View style={styles.container}> <Text style={styles.title}>Details</Text> </View>); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, title: { fontSize: 50, } });
Create Screen Index File
Let's gather all our screens into one file and re-export them. This makes importing them into our navigators easier.
// screens/index.ts export { default as Home } from '@/screens/HomeScreen'; export { default as Settings } from '@/screens/SettingsScreen'; export { default as Details } from '@/screens/DetailsScreen';
Setup Drawer Navigator
Now that we have our screens setup, let's configure our first navigator; the drawer navigator.
Install Dependencies
% yarn add @react-navigation/drawer % expo install react-native-gesture-handler react-native-reanimated
Create Navigator
// src/navigators/DrawerNavigator/DrawerNavigator.tsx import React from 'react'; import { createDrawerNavigator } from '@react-navigation/drawer'; import { HomeScreen, SettingsScreen } from '@/screens/index'; const Drawer = createDrawerNavigator(); export default function DrawerNavigator(): JSX.Element { return ( <Drawer.Navigator> <Drawer.Screen name='Home' component={HomeScreen} /> <Drawer.Screen name='Settings' component={SettingsScreen} /> </Drawer.Navigator>); }
Replace App.tsx
In order to use our new navigator, let's comment out our current
App.tsx
code and replace it with the following,// src/App.tsx import React from 'react'; import { NavigationContainer } from '@react-navigation/native'; import DrawerNavigator from '@/navigators/DrawerNavigator'; export default function App() { return ( <NavigationContainer> <DrawerNavigator /> </NavigationContainer>); }
Setup Tab Navigator
With the drawer navigation working, let's add tab navigation to our Home screen.
Install Dependencies
% yarn add @react-navigation/bottom-tabs
Create Tab Navigator
// src/navigators/TabNavigator/TabNavigator.tsx import React from 'react'; import { Button } from 'react-native'; import { CompositeNavigationProp } from '@react-navigation/native'; import { DrawerNavigationProp } from '@react-navigation/drawer'; import { createBottomTabNavigator, BottomTabNavigationProp } from '@react-navigation/bottom-tabs'; import { HomeScreen, DetailsScreen } from '@/screens/index'; export type ParamList = { 'Home': undefined, 'Details': undefined }; type NavigationProp = CompositeNavigationProp< BottomTabNavigationProp<ParamList, 'Home'>, DrawerNavigationProp<ParamList> >; type Props = { navigation: NavigationProp; } const Tabs = createBottomTabNavigator(); export default function TabNavigator({ navigation }: Props): JSX.Element { return ( <Tabs.Navigator screenOptions={{ headerRight: () => ( <ButtononPress={() => navigation.toggleDrawer()}title="Menu"color="black"/>), }}> <Tabs.Screen name='Home' component={HomeScreen} /> <Tabs.Screen name='Details' component={DetailsScreen} /> </Tabs.Navigator> ); }
We need to add the headerRight option in order to replace the drawer toggle button that isn't there anymore since we are hiding the drawer header. I put it on the right because when we add stack navigation later, the back button will be in the left spot.
Create Drawer and Tab Navigator
// src/navigators/DrawerWithTabsNavigator.tsx import React from 'react'; import { createDrawerNavigator } from '@react-navigation/drawer'; import TabNavigator from '@/navigators/TabNavigator'; import { HomeScreen, SettingsScreen } from '@/screens/index'; const Drawer = createDrawerNavigator(); export default function DrawerWithTabsNavigator(): JSX.Element { return ( <Drawer.Navigator> <Drawer.Screen name='HomeTabNavigator' component={TabNavigator} options={{ title: 'Home', headerShown: false }} /> <Drawer.Screen name='Settings' component={SettingsScreen} /> </Drawer.Navigator>); }
The headerShown: false is necessary so we don't have two 'Home' header bars; one from the drawer navigator and another from the tab navigator. Also, we have to specify the title 'Home' because title defaults to the screen name which in this case is 'HomeTabNavigator' since we can't repeat the same name in the same chain of navigators.
Update App.tsx
Now, let's replace our current
DrawerNavigator
with our new DrawerWithTabsNavigator
.// src/App.tsx import React from 'react'; import { NavigationContainer } from '@react-navigation/native'; import DrawerWithTabsNavigator from '@/navigators/DrawerWithTabsNavigator'; export default function App() { return ( <NavigationContainer> <DrawerWithTabsNavigator /> </NavigationContainer>); }
Setup Stack Navigator
For the final navigator, let's add a stack navigator to our Home screen.
Install Dependencies
% yarn add @react-navigation/native-stack
Create Stack Navigator
// src/navigators/StackNavigator/StackNavigator.tsx import React from 'react'; import { Button } from 'react-native'; import { createNativeStackNavigator, NativeStackNavigationProp } from '@react-navigation/native-stack'; import { CompositeNavigationProp } from '@react-navigation/native'; import { DrawerNavigationProp } from '@react-navigation/drawer'; import { HomeScreen, SettingsScreen } from '@/screens/index'; export type ParamList = { 'Home': undefined, 'Settings': undefined }; type NavigationProp = CompositeNavigationProp< NativeStackNavigationProp<ParamList, 'Home'>, DrawerNavigationProp<ParamList> >; type Props = { navigation: NavigationProp } const Stack = createNativeStackNavigator(); export default function StackNavigator({ navigation }: Props): JSX.Element { return ( <Stack.Navigator screenOptions={{ headerRight: () => ( <ButtononPress={() => navigation.toggleDrawer()}title="Menu"color="black"/>), }}> <Stack.Screen name={'Home'} component={HomeScreen} /> <Stack.Screen name={'Settings'} component={SettingsScreen} /> </Stack.Navigator> ); }
We need to add the headerRight option in order to replace the drawer toggle button that isn't there anymore since we are hiding the drawer header. I put it on the right because the stack navigation back button will be in the left spot.
Create Tab and Stack Navigator
import React from 'react'; import { Button } from 'react-native'; import { createBottomTabNavigator, BottomTabNavigationProp } from '@react-navigation/bottom-tabs'; import StackNavigator from '@/navigators/StackNavigator'; import { CompositeNavigationProp } from '@react-navigation/native'; import { DrawerNavigationProp } from '@react-navigation/drawer'; import { DetailsScreen } from '@/screens/index'; export type ParamList = { 'HomeStack': undefined, 'Details': undefined }; type NavigationProp = CompositeNavigationProp< BottomTabNavigationProp<ParamList, 'HomeStack'>, DrawerNavigationProp<ParamList> >; type Props = { navigation: NavigationProp } const Tabs = createBottomTabNavigator(); export default function TabWithStackNavigator({ navigation }: Props): JSX.Element { return ( <Tabs.Navigator screenOptions={{ headerRight: () => ( <ButtononPress={() => navigation.toggleDrawer()}title="Menu"color="black"/>), }}> <Tabs.Screen name={'HomeStack'} component={StackNavigator} options={{ title: 'Home', headerShown: false }}/> <Tabs.Screen name={'Details'} component={DetailsScreen} /> </Tabs.Navigator> ); }
The headerShown: false is necessary so we don't have two 'Home' header bars; one from the drawer navigator and another from the tab navigator. Also, we have to specify the title 'Home' because title defaults to the screen name which in this case is 'HomeStack' since we can't repeat the same name in the same chain of navigators.
Create Drawer with Tab and Stack Navigator
// src/navigators/DrawerWithTabsAndStackNavigator/DrawerWithTabsAndStackNavigator.tsx import React from 'react'; import { createDrawerNavigator } from '@react-navigation/drawer'; import TabWithStackNavigator from '../TabWithStackNavigator'; import { SettingsScreen } from '@/screens/index'; const Drawer = createDrawerNavigator(); export default function DrawerWithTabsAndStackNavigator(): JSX.Element { return ( <Drawer.Navigator> <Drawer.Screen name="HomeTabWithStackNavigator" component={TabWithStackNavigator} options={{ title: 'Home', headerShown: false }} /> <Drawer.Screen name="Settings" component={SettingsScreen} /> </Drawer.Navigator> ); }
The headerShown: false is necessary so we don't have two 'Home' header bars; one from the drawer navigator and another from the tab navigator. Also, we have to specify the title 'Home' because title defaults to the screen name which in this case is 'HomeStack' since we can't repeat the same name in the same chain of navigators.
Update App.tsx
Okay, our final
App.tsx
update. Let's replace the current navigator with our new DrawerWithTabAndStackNavigator
.// src/App.tsx import React from 'react'; import { NavigationContainer } from '@react-navigation/native'; import DrawerWithTabAndStackNavigator from '@/navigators/DrawerWithTabAndStackNavigator'; export default function App() { return ( <NavigationContainer> <DrawerWithTabAndStackNavigator /> </NavigationContainer> ); }
Update Home Screen
Now that the navigation is all set, let's move our original
App.tsx
code to our Home Screen.// src/screens/HomeScreen/HomeScreen.tsx import React, { useEffect, useState } from 'react'; import { View, Text, Button, StyleSheet } from 'react-native'; import { CompositeNavigationProp } from '@react-navigation/native'; import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { Analytics } from 'aws-amplify'; import { StatusBar } from 'expo-status-bar'; import { LocationObject } from 'expo-location'; import { SearchPlaceIndexForPositionResponse } from 'aws-sdk/clients/location'; import LOGGER from '@/services/logger'; import Sentry, { initSentry } from '@/services/sentry'; import { initAmplify } from '@/api/amplify'; import { initLocation, getLocation, getReverseGeocode } from '@/services/location'; import { updateEndpointLocation } from '@/services/analytics'; import { SomeUtility } from '@/utilities/testUtility'; LOGGER.enable('APP'); const log = LOGGER.extend('APP'); initSentry(false); log.info('app log test'); type TabParamList = { Home: undefined, Details: undefined } type StackParamList = { Home: undefined; Settings: undefined; } type NavigationProps = CompositeNavigationProp< BottomTabNavigationProp<TabParamList, 'Home'>, NativeStackNavigationProp<StackParamList> >; type Props = { navigation: NavigationProps; } export default function HomeScreen({ navigation }: Props) { const [location, setLocation] = useState<LocationObject>(); const [reverseGeocode, setReverseGeocode] = useState<SearchPlaceIndexForPositionResponse>(); useEffect(() => { (async () => { await initAmplify(); const locEnabled = await initLocation(); const loc = await getLocation(); setLocation(loc); // TODO: If cannot access device location, use IP to Location API instead if (locEnabled) { const reverseGeo = await getReverseGeocode(loc.coords); setReverseGeocode(reverseGeo); await updateEndpointLocation(); } })(); }, []); const rgc = reverseGeocode?.Results[0].Place; return ( <View style={styles.container}> <Text>process.env.NODE_ENV: {process.env.NODE_ENV}</Text> <Text>process.env.NAME: {process.env.NAME}</Text> <Text>Path Alias: {SomeUtility()}</Text> <Text>- Location -</Text> <Text>Latitude: {location?.coords.latitude}</Text> <Text>Longitude: {location?.coords.longitude}</Text> <Text>- Reverse Geocode -</Text> <Text>{rgc?.Label}</Text> <Button title="Press to cause error!" onPress={() => { log.error('Oh no!!!'); Sentry.captureException(new Error('Oops!')); }}/> <Button title="Press to cause analytics event!" onPress={() => { Analytics.record({ name: 'buttonClick' }); }}/> <Button title='Navigate to Settings' onPress={() => navigation.navigate('Settings')} /> {/* eslint-disable-next-line */} <StatusBar style="auto" /> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, });