Expo Starter: Navigation
ūüďĪ

Expo Starter: Navigation

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.
notion image

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', }, });
Click to rocket boost to the top of the page!