Chapter 6: React Navigation
Full Stack TypeScript
link React Navigation
By the end of this lesson, developers will be able to:
- Utilize the latest stable React Navigation version (v5)
- Create tabbed navigation with detail routes
- Create an animated drawer navigator
Introduction
React Navigation has made lots of improvements to support full cross-platform functionality. In this section, we implement the latest version and see how a React Native app works on iOS, Android and Web.
Setting Up
Let's start by opening and running the app project.
cd <app-name>
yarn start
Once you have confirmed the app project is running, we will add navigation to the React Native project.
Heads Up! This lesson's code edits will only affect your React Native project.
link React Navigation Stack
React Navigation will allow us to create a navigation stack to transition between screens. Let's install their required dependencies:
yarn add @react-navigation/native @react-navigation/stack @react-native-community/masked-view @react-navigation/drawer @react-navigation/material-bottom-tabs react-native-paper
expo install react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context
For more info on
react-navigation
, click here.
link Drawer Content
Our app will feature drawer navigation, similar to the Twitter app. Getting started, we will create a new file called src/navigation/DrawerContent.tsx
:
import React from 'react';
import { View, StyleSheet, TouchableOpacity } from 'react-native';
import Animated from 'react-native-reanimated';
import { DrawerItem, DrawerContentScrollView } from '@react-navigation/drawer';
import {
useTheme,
Title,
Caption,
Paragraph,
Drawer,
Text,
TouchableRipple,
Switch
} from 'react-native-paper';
import { MaterialCommunityIcons } from '@expo/vector-icons';
export function DrawerContent(props) {
const paperTheme = useTheme();
const translateX = Animated.interpolate(props.progress, {
inputRange: [0, 0.5, 0.7, 0.8, 1],
outputRange: [-100, -85, -70, -45, 0]
});
return (
<DrawerContentScrollView {...props}>
<Animated.View
//@ts-ignore
style={[
styles.drawerContent,
{
backgroundColor: paperTheme.colors.surface,
transform: [{ translateX }]
}
]}
>
<View style={styles.drawerContent}>
<View style={styles.userInfoSection}>
<TouchableOpacity
style={{ marginLeft: 10 }}
onPress={() => {
props.navigation.toggleDrawer();
}}
>
<MaterialCommunityIcons
color={paperTheme.colors.text}
name="account-outline"
size={50}
/>
</TouchableOpacity>
<Title style={styles.title}>Guest</Title>
<Caption style={styles.caption}>@</Caption>
<View style={styles.row}>
<View style={styles.section}>
<Paragraph style={[styles.paragraph, styles.caption]}>
--
</Paragraph>
<Caption style={styles.caption}>Following</Caption>
</View>
<View style={styles.section}>
<Paragraph style={[styles.paragraph, styles.caption]}>
--
</Paragraph>
<Caption style={styles.caption}>Followers</Caption>
</View>
</View>
</View>
<Drawer.Section style={styles.drawerSection}>
<DrawerItem
icon={({ color, size }) => (
<MaterialCommunityIcons
name="account-outline"
color={color}
size={size}
/>
)}
label="Profile"
onPress={() => {}}
/>
<DrawerItem
icon={({ color, size }) => (
<MaterialCommunityIcons name="tune" color={color} size={size} />
)}
label="Preferences"
onPress={() => {}}
/>
<DrawerItem
icon={({ color, size }) => (
<MaterialCommunityIcons
name="bookmark-outline"
color={color}
size={size}
/>
)}
label="Bookmarks"
onPress={() => {}}
/>
</Drawer.Section>
<Drawer.Section title="Preferences">
<TouchableRipple onPress={props.toggleTheme}>
<View style={styles.preference}>
<Text>Dark Theme</Text>
<View pointerEvents="none">
<Switch value={paperTheme.dark} />
</View>
</View>
</TouchableRipple>
</Drawer.Section>
</View>
</Animated.View>
</DrawerContentScrollView>
);
}
const styles = StyleSheet.create({
drawerContent: {
flex: 1
},
userInfoSection: {
paddingLeft: 20
},
title: {
marginTop: 20,
fontWeight: 'bold'
},
caption: {
fontSize: 14,
lineHeight: 14
},
row: {
marginTop: 20,
flexDirection: 'row',
alignItems: 'center'
},
section: {
flexDirection: 'row',
alignItems: 'center',
marginRight: 15
},
paragraph: {
fontWeight: 'bold',
marginRight: 3
},
drawerSection: {
marginTop: 15
},
preference: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 12,
paddingHorizontal: 16
}
});
link Drawer Navigator
Let's integrate the DrawerContent
with a new file called src/navigation/AppNavigator.ts
:
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import {
createDrawerNavigator,
DrawerContentOptions
} from '@react-navigation/drawer';
import { DefaultTheme, DarkTheme } from '@react-navigation/native';
import { useTheme } from 'react-native-paper';
import { DrawerContent } from './DrawerContent';
import { Places } from '../screens';
// import { MainTabNavigator } from './MainTabNavigator';
const Drawer = createDrawerNavigator();
const AppNavigator = navProps => {
const theme = useTheme();
const navigationTheme = theme.dark ? DarkTheme : DefaultTheme;
return (
<NavigationContainer theme={navigationTheme}>
<Drawer.Navigator
drawerContent={props => (
<DrawerContent {...props} toggleTheme={navProps.toggleTheme} />
)}
>
<Drawer.Screen name="Home" component={Places} />
</Drawer.Navigator>
</NavigationContainer>
);
};
export default AppNavigator;
Be sure to add a new index.ts
file inside src/navigation
:
import AppNavigator from './AppNavigator';
export { AppNavigator };
link App Navigator
To see the Places screen, we will render the AppNavigator.ts
inside our parent App.ts
component:
import React from 'react';
import {
DarkTheme as PaperDarkTheme,
DefaultTheme as PaperDefaultTheme,
Provider as PaperProvider
} from 'react-native-paper';
import {
DarkTheme as NavigationDarkTheme,
DefaultTheme as NavigationDefaultTheme
} from '@react-navigation/native';
import { ApolloProvider } from '@apollo/react-hooks';
import { apolloClient } from './graphql';
import { AppNavigator } from './src/navigation';
const CombinedDefaultTheme = {
...PaperDefaultTheme,
...NavigationDefaultTheme
};
const CombinedDarkTheme = {
...PaperDarkTheme,
...NavigationDarkTheme,
colors: { ...PaperDarkTheme.colors, ...NavigationDarkTheme.colors }
};
export default function App() {
const [isDarkTheme, setIsDarkTheme] = React.useState(false);
const theme = isDarkTheme ? CombinedDarkTheme : CombinedDefaultTheme; // Use Light/Dark theme based on a state
function toggleTheme() {
// We will pass this function to Drawer and invoke it on theme switch press
setIsDarkTheme(isDark => !isDark);
}
return (
<ApolloProvider client={apolloClient}>
<PaperProvider theme={theme as any}>
<AppNavigator toggleTheme={toggleTheme} />
</PaperProvider>
</ApolloProvider>
);
}
Save and reload the simulator.
- Swipe right to see our new drawer navigation.
- Try the new "dark mode" toggle switch.
- If the server is running, a list of places should still be visible.
link Place Detail
Let's create a new screen called PlaceDetail
.
import React from 'react';
import { SafeAreaView } from 'react-native';
import { CardView } from '../components';
interface Props {
navigation;
}
const PlaceDetail: React.FC<Props> = props => {
const { navigation } = props;
const { item } = navigation.state.params;
return (
<SafeAreaView>
<CardView {...(item as any)} />
</SafeAreaView>
);
};
export default PlaceDetail;
Be sure to add the new screen to your src/screens/index.ts
:
import Places from './Places';
+import PlaceDetail from './PlaceDetail';
-export { Places };
+export { Places, PlaceDetail };
Now when we click any Place, we can navigate to that Place's detail screen.
link Header
We can use a custom AppBar
to support theming for our app's navigation header.
Create a new file called src/navigation/Header.tsx
:
import React from 'react';
import { AsyncStorage, TouchableOpacity } from 'react-native';
import { Appbar, useTheme } from 'react-native-paper';
import { MaterialCommunityIcons } from '@expo/vector-icons';
export const Header = ({ scene, previous, navigation }) => {
const theme = useTheme();
const { options } = scene.descriptor;
const title =
options.headerTitle !== undefined
? options.headerTitle
: options.title !== undefined
? options.title
: scene.route.name;
return (
<Appbar.Header theme={{ colors: { primary: theme.colors.primary } }}>
{previous ? (
<Appbar.BackAction
onPress={navigation.goBack}
color={theme.colors.text}
/>
) : (
<TouchableOpacity
onPress={() => {
navigation.openDrawer();
}}
>
<MaterialCommunityIcons
color={theme.colors.text}
name="menu"
size={30}
/>
</TouchableOpacity>
)}
<Appbar.Content
title={
previous ? (
title
) : (
<MaterialCommunityIcons name="home-outline" size={40} />
)
}
/>
{title == 'Profile' && (
<Appbar.Action
icon="logout"
onPress={async () => {
await AsyncStorage.removeItem('token');
navigation.replace('Login');
}}
/>
)}
</Appbar.Header>
);
};
link TabNavigator
It's time to set up our tab navigator.
Create a new file called src/navigation/MainTabNavigator.tsx
.
import React from 'react';
import { Platform, AsyncStorage } from 'react-native';
import { createStackNavigator } from '@react-navigation/stack';
import { createMaterialBottomTabNavigator } from '@react-navigation/material-bottom-tabs';
import { Places } from '../screens';
import { useTheme, Portal, FAB } from 'react-native-paper';
import { useIsFocused } from '@react-navigation/native';
import { PlaceDetail } from '../screens';
import { Header } from './Header';
const Stack = createStackNavigator();
export const PlaceStack = () => {
return (
<Stack.Navigator
initialRouteName="Places"
headerMode="screen"
screenOptions={{
header: ({ scene, previous, navigation }) => (
<Header scene={scene} previous={previous} navigation={navigation} />
)
}}
>
<Stack.Screen name="Places" component={Places} />
<Stack.Screen name="Detail" component={PlaceDetail} />
</Stack.Navigator>
);
};
const Tab = createMaterialBottomTabNavigator();
export const MainTabNavigator = () => {
const isFocused = useIsFocused();
const theme = useTheme();
return (
<React.Fragment>
<Tab.Navigator
initialRouteName="Places"
// shifting={true}
sceneAnimationEnabled={false}
>
<Tab.Screen
name="Places"
component={PlaceStack}
options={{
tabBarIcon: 'home-account'
}}
/>
</Tab.Navigator>
<Portal>
<FAB
visible={isFocused}
icon="feather"
onPress={() => console.log('pressed FAB')}
style={{
backgroundColor: theme.colors.background,
position: 'absolute',
bottom: 100,
right: 16
}}
/>
</Portal>
</React.Fragment>
);
};
MainTabNavigator
Import the MainTabNavigator
inside the AppNavigator.tsx
:
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import {
createDrawerNavigator,
DrawerContentOptions
} from '@react-navigation/drawer';
import { DefaultTheme, DarkTheme } from '@react-navigation/native';
import { useTheme } from 'react-native-paper';
import { DrawerContent } from './DrawerContent';
import { MainTabNavigator } from './MainTabNavigator';
const Drawer = createDrawerNavigator();
const AppNavigator = navProps => {
const theme = useTheme();
const navigationTheme = theme.dark ? DarkTheme : DefaultTheme;
return (
<NavigationContainer theme={navigationTheme}>
<Drawer.Navigator
drawerContent={props => (
<DrawerContent {...props} toggleTheme={navProps.toggleTheme} />
)}
>
<Drawer.Screen name="Home" component={MainTabNavigator} />
</Drawer.Navigator>
</NavigationContainer>
);
};
export default AppNavigator;
To see the changes in action, reload the application. You may see two tabs, one with a loading indicator.
WELL DONE!
Wrapping Up
Today we utilized React Navigation to create tab and drawer navigation. We even support app theming, with a "dark mode" toggle. Here's a full list of resources for this lecture: