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:

format_list_bulleted
help_outline