Getting Started with React Navigation v6 and TypeScript in React Native

Published on Jun 11, 2022

14 min read

REACT-NATIVE

cover_image

Originally published at Jscrambler.com

When you have a complex mobile application structure or many screens in your application, handling navigation can become tricky. However, with open-source libraries like React Navigation, the process of implementing navigation patterns does become easier.

React Navigation library is one of the most used navigation libraries in React Native ecosystem. It is written in TypeScript, and you can create React components and apply any navigation patterns from Stack, Tab, and Drawer.

In this tutorial, let's look at how you can set up and use React Navigation and TypeScript together in a React Native app. One cool advantage that TypeScript provides is type checking for route names and route parameters.

Pre-requisites

🔗

If you are going to code along, make sure you have already installed the following:

  • Nodejs (>=12.x.x) with npm/yarn installed
  • expo-cli
  • Access to a real device or an iOS simulator or an Android Emulator so that you can run your code example

The source code used in this tutorial is available at this GitHub repository.

Creating a React Native project with expo-cli

🔗

Before diving deep into configuring TypeScript with React Navigation, let us create an example app that uses React Navigation to navigate between different screens. This example screen will also have example screens.

You can skip this section if you are already familiar with Stack and Tab navigators in React Navigation.

Open the terminal and run the following command to create a new React Native app. When asked to "choose a template", select blank (TypeScript). This template creates a React Native project with TypeScript already configured. Enough for us to get started.

expo init myApp
# This will prompt a "Choose a template" question
# Select "blank (TypeScript)"
# After the project is created, navigate inside the project directory
cd myApp

After navigating inside the project directory, run the following command to install React Navigation libraries and its packages in the terminal window.

yarn add @react-navigation/native @react-navigation/bottom-tabs @react-navigation/native-stack && expo install react-native-screens react-native-safe-area-context

The above command will install packages for implementing Stack and Tabs navigators. In the example app, we will use both of these patterns.

Adding a stack navigator

🔗

React Navigation's stack navigator allows your app to transition between screens and manage navigation history. The stack navigator you will implement in this section will allow the app user to navigate from one screen to another.

Start by creating a src/ directory that will contain the screen and navigation related code files.

The next step in the example app is to create mock screens. Create a screens/ directory inside src/. Inside this directory, let's create four component files:

  • HomeScreen.tsx
  • DetailsScreen.tsx

The HomeScreen component displays a list of characters from the Star Wars API. On pressing any item from the list, the app user will be able to navigate to the DetailsScreen where they can view the details of each character.

Add the following code snippet to the HomeScreen.tsx:

1import { StyleSheet, View, Text, Pressable, FlatList } from 'react-native';
2import { useNavigation } from '@react-navigation/native';
3
4const DATA = [
5 {
6 id: 1,
7 name: 'Luke Skywalker',
8 birth_year: '19BBY'
9 },
10 {
11 id: 2,
12 name: 'C-3PO',
13 birth_year: '112BBY'
14 },
15 {
16 id: 3,
17 name: 'R2-D2',
18 birth_year: '33BBY'
19 },
20 {
21 id: 4,
22 name: 'Darth Vader',
23 birth_year: '41.9BBY'
24 },
25 {
26 id: 5,
27 name: 'Leia Organa',
28 birth_year: '19BBY'
29 }
30];
31
32const HomeScreen = () => {
33 const navigation = useNavigation();
34
35 const renderListItems = ({ item }) => {
36 return (
37 <Pressable
38 onPress={() =>
39 navigation.navigate('Details', {
40 name: item.name,
41 birthYear: item.birth_year
42 })
43 }
44 >
45 <Text
46 style={{ fontSize: 18, paddingHorizontal: 12, paddingVertical: 12 }}
47 >
48 {item.name}
49 </Text>
50 <View
51 style={{
52 borderWidth: StyleSheet.hairlineWidth,
53 borderColor: '#ccc'
54 }}
55 />
56 </Pressable>
57 );
58 };
59
60 return (
61 <View style={{ flex: 1, paddingTop: 10 }}>
62 <FlatList data={DATA} renderItem={renderListItems} />
63 </View>
64 );
65};
66
67export default HomeScreen;

In the above code snippet, observe that the onPress prop on the Pressable component is used to pass the name and birthYear of the character to the Details screen as route parameters.

Add the following code snippet to the DetailsScreen.tsx:

1import { View, Text } from 'react-native';
2import { useRoute } from '@react-navigation/native';
3
4const DetailScreen = () => {
5 const route = useRoute();
6 const { name, birthYear } = route.params;
7
8 return (
9 <View style={{ flex: 1, paddingTop: 12, paddingHorizontal: 10 }}>
10 <Text style={{ fontSize: 18, paddingBottom: 12 }}>Name: {name}</Text>
11 <Text style={{ fontSize: 18 }}>Birth Year: {birthYear}</Text>
12 </View>
13 );
14};
15
16export default DetailScreen;

In the above code snippet, notice that the route.params is used to read the parameters passed from the HomeScreen.

After setting up the screens, create the navigation/ directory inside the src/ and inside it, add two files:

  • index.tsx: to keep the Root Navigator configuration
  • HomeStack.tsx: to create a Stack Navigator for Home and Details screens

Inside the HomeStack.tsx file, add the following code snippet:

1import * as React from 'react';
2import { createNativeStackNavigator } from '@react-navigation/native-stack';
3
4import HomeScreen from '../screens/HomeScreen';
5import DetailsScreen from '../screens/DetailsScreen';
6
7const HomeStack = createNativeStackNavigator();
8
9const HomeStackNavigator = () => {
10 return (
11 <HomeStack.Navigator>
12 <HomeStack.Screen name="Home" component={HomeScreen} />
13 <HomeStack.Screen name="Details" component={DetailsScreen} />
14 </HomeStack.Navigator>
15 );
16};
17
18export default HomeStackNavigator;

In the above code snippet, notice that the name prop on the HomeStack.Screen component is used to define the route name. For example, the DetailsScreen has the route name defined as Details. Any time you navigate the Details screen, the route name is used to identify the screen either in the navigation.navigate() or navigation.push() method.

Next, add the following code snippet to the index.tsx file:

1import * as React from 'react';
2import { NavigationContainer } from '@react-navigation/native';
3
4import HomeStackNavigator from './HomeStack';
5
6const RootNavigator = () => {
7 return (
8 <NavigationContainer>
9 <HomeStackNavigator />
10 </NavigationContainer>
11 );
12};
13
14export default RootNavigator;

Lastly, modify the App.tsx file to include the RootNavigator component:

1import { StatusBar } from 'expo-status-bar';
2
3import RootNavigator from './src/navigation';
4
5export default function App() {
6 return (
7 <>
8 <RootNavigator />
9 <StatusBar style="auto" />
10 </>
11 );
12}

The HomeStack navigator configuration is done. Next, run any of the following commands to see the navigator in action:

# for iOS
expo start --ios
# for Android
expo start --android

Here is the output you will get after this step:

ss1

Adding type checking for stack navigator

🔗

To type check route name and parameters in both the HomeStack and RootStack navigators, you need to create type mappings for each route name and params.

Start by creating a new file called types.ts inside the src/navigation/ directory. This file will contain mappings for all route names and route params. Throughout this tutorial, it is used to define types for each type of navigator.

Inside the file types.ts, define the types for the route names: Home and Details.

1export type HomeStackNavigatorParamList = {
2 Home: undefined;
3 Details: {
4 name: string;
5 birthYear: string;
6 };
7};

A route name that doesn't have any parameters being passed is specified with undefined. So, for example, in the above snippet, the Home route name doesn't have any parameters being passed to it.

The Details route receives two parameters from the HomeScreen. This is why the mapping object contains two properties in the above snippet.

After creating the mappings, you must let the stack navigator know about them. Go back to the HomeStack.tsx file and inside it, import HomeStackNavigatorParamList. It is passed as a generic to the createNativeStackNavigator function.

1// rest of the import statements remain same
2import { HomeStackNavigatorParamList } from './types';
3
4const HomeStack = createNativeStackNavigator<HomeStackNavigatorParamList>();
5
6// rest of the code remains the same

Testing type checking and IntelliSense

🔗

All the configurations in the previous section will enable type checking for the HomeStack navigator. For example, in the HomeStack.tsx file, change the name of the Details to Detail.

1<HomeStack.Screen name="Detail" component={DetailsScreen} />

After the modification, you will see a red squiggly line appears on the name prop.

ss3

If you hover over the name prop, it will show a similar error message like the following:

ss4

The HomeStack navigator expects a HomeStackNavigatorParamList type with either Home or Details.

Another advantage of type checking is that it provides intelliSense for navigator props (depending on which IDE or Code editor you are using). For large applications where there are a lot of screens, this helps. You do not have to remember each route params for every screen.

Adding type checks for screens

🔗

In this section, let's learn how to add type checking for the Home screen. It is the screen where an app user will interact with a button to navigate to the Details screen.

To add type checking for a screen, the first step is to use a generic type to define types for the individual screens. Each navigator pattern in React Navigation library exposes its own generic type. For example, NativeStackNavigationProp is used for @react-navigation/native-stack. Import that in the types.ts file.

1import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
2
3export type HomeStackNavigatorParamList = {
4 Home: undefined;
5 Details: {
6 name: string;
7 birthYear: string;
8 };
9};
10
11export type HomeScreenNavigationProp = NativeStackNavigationProp<
12 HomeStackNavigatorParamList,
13 'Details'
14>;

The NativeStackNavigationProp accept two parameters. The first is the type that maps the route names and their params. Hence, the navigator itself. The second is the name of the screen as a string that matches the route name from the first parameter. In the above code snippet, the first parameter is HomeStackNavigatorParamList, and the second parmater, in this case, can only be Details.

The second parameter of NativeStackNavigationProp represents that the Home screen gets only the described route name as a possibility that the Home screen can navigate to. If the second parameter is not defined, then the Home screen will get all the route names from the HomeStack navigator as possibilities that it can navigate to.

Now, open the HomeScreen file, import the HomeScreeProps type, and use it to annotate the useNavigation hook.

1// after other import statements
2
3// import HomeScreenNavigationProp
4import { HomeScreenNavigationProp } from '../navigation/types';
5
6// inside the HomeScreen, component modify the following line
7const HomeScreen = () => {
8 const navigation = useNavigation<HomeScreenNavigationProp>();
9
10 // rest of the code remains the same
11};

If you are using the navigation prop directly on the functional component, you can pass the HomeScreenNavigationProp type to the functional component.

1function HomeScreen({ navigation }: HomeScreenNavigationProp) {
2 // ...
3}

If you are using @react-navigation/stack, you can use StackScreenProps instead of StackNavigationProp.

Adding type checks for route params

🔗

To add type checking for a screen that receives route params (for example, in the example app Details screen receives two route params), you need to import the RouteProp from @react-navigation/native.

After importing it, create a type for the Details screen using the HomeStackNavigatorParamList as the first parameter and the route name of the Details screen as the second parameter to the RouteProp.

1// after other import statements
2import type { RouteProp } from '@react-navigation/native';
3
4export type DetailsScreenRouteProp = RouteProp<
5 HomeStackNavigatorParamList,
6 'Details'
7>;
8
9// rest of the code remains the same

Open the DetailsScreen file, import the DetailsScreenRouteProp type, and use it to annotate the useRoute hook.

1// after other import statements
2import { DetailsScreenRouteProp } from '../navigation/types';
3
4// inside the DetailsScreen, the component modify the following line
5
6const DetailScreen = () => {
7 const route = useRoute<DetailsScreenRouteProp>();
8
9 // rest of the code remains the same
10};

Adding a bottom navigator

🔗

Let's continue the saga of adding type checks to the app screens by adding a Bottom Tab Navigator to the example app. We have already installed the bottom tabs package when creating the example app.

Let's add two more screens to the src/screens/ directory. Inside it, create a new file FeedScreen.tsx and add the following code snippet:

1import { View, Text } from 'react-native';
2
3const FeedScreen = () => {
4 return (
5 <View style={{ flex: 1, paddingTop: 12, paddingHorizontal: 10 }}>
6 <Text style={{ fontSize: 18 }}>Feed Screen</Text>
7 </View>
8 );
9};
10
11export default FeedScreen;

Create another new file called SettingsScreen.tsx and add the following code snippet:

1import { View, Text } from 'react-native';
2
3const SettingsScreen = () => {
4 return (
5 <View style={{ flex: 1, paddingTop: 12, paddingHorizontal: 10 }}>
6 <Text style={{ fontSize: 18 }}>Settings Screen</Text>
7 </View>
8 );
9};
10
11export default SettingsScreen;

Next, add the type check mapping objects for the bottom tab navigator in the src/navigation/types.ts file. The name of the navigator is BottomTabNavigatorParamList.

The bottom tab navigator will contain the Home screen as its first tab. The second tab will be the Feed screen. The third tab will be the Settings screen. You can specify HomeStackNavigatorParamList as the value for the Home key.

1export type BottomTabNavigatorParamList = {
2 Home: HomeStackNavigatorParamList;
3 Feed: undefined;
4 Settings: undefined;
5};

Inside the src/navigation/ directory, add a new file called Tabs.tsx with the following code snippet:

1import * as React from 'react';
2import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
3
4import { BottomTabNavigatorParamList } from './types';
5import HomeStackNavigator from './HomeStack';
6import FeedScreen from '../screens/FeedScreen';
7import SettingsScreen from '../screens/SettingsScreen';
8
9const Tab = createBottomTabNavigator<BottomTabNavigatorParamList>();
10
11const BottomTabs = () => {
12 return (
13 <Tab.Navigator>
14 <Tab.Screen
15 name="HomeStack"
16 component={HomeStackNavigator}
17 options={{ headerShown: false }}
18 />
19 <Tab.Screen name="Feed" component={FeedScreen} />
20 <Tab.Screen name="Settings" component={SettingsScreen} />
21 </Tab.Navigator>
22 );
23};
24
25export default BottomTabs;

Annotating types for the bottom tab navigator with BottomTabNavigatorParamList will add type checks for each screen in the tab navigator.

Let's also modify the src/navigation/index.tsx file to replace the previous HomeStack by importing the BottomTabs component and rendering it.

1// rest of the import statements remain same
2import BottomTabs from './Tabs';
3
4const RootNavigator = () => {
5 return (
6 <NavigationContainer>
7 <BottomTabs />
8 </NavigationContainer>
9 );
10};
11
12export default RootNavigator;

Here is the output you get after this step:

ss5

Composing nested navigator types

🔗

In the current state of the example app, you will notice that the HomeStack navigator is now nested inside the bottom tab navigator.

Let's assume, for some reason, you want to provide a button for the app user to navigate from the Home screen to the Feed screen. This is doable since both of these screens share the same parent navigator.

Add a button above the FlatList in the HomeScreen.tsx file that allows an app user to navigate to the Feed screen as shown below:

1return (
2 <View style={{ flex: 1, paddingTop: 10 }}>
3 <Pressable
4 onPress={() => navigation.navigate('Feed')}
5 style={{
6 padding: 8,
7 borderWidth: 1,
8 borderRadius: 4,
9 borderColor: 'red',
10 margin: 12,
11 alignItems: 'center'
12 }}
13 >
14 <Text style={{ fontSize: 16, fontWeight: '600' }}>Go to Feed screen</Text>
15 </Pressable>
16 <FlatList data={DATA} renderItem={renderListItems} />
17 </View>
18);

Here is how the button looks on the Home screen:

ss6

If you look closely at the JSX just added, a red squiggly line has appeared underneath Feed.

ss7

The error states that the Feed screen is not part of the HomeScreenNavigationProp, which is true because the Feed screen is not part of the param list we defined for the Home stack navigator in the src/navigation/types.tsx file.

React Navigation library exposes the CompositeNavigationProp type that allows annotating the navigation prop when nesting navigators. It takes two parameters. The first parameter is the primary navigator, in this case, the Home Stack navigator itself. The second parameter is the type of a parent navigator or any other source of secondary navigation. In this case, it will be the bottom tab navigator.

Modify the type HomeScreenNavigationProp as shown below:

1import type {
2 CompositeNavigationProp,
3 RouteProp
4} from '@react-navigation/native';
5// rest of the import statements remains same
6
7export type HomeScreenNavigationProp = CompositeNavigationProp<
8 NativeStackNavigationProp<HomeStackNavigatorParamList, 'Details'>,
9 BottomTabNavigationProp<BottomTabNavigatorParamList, 'Feed'>
10>;
11
12// rest of the code remains the same

If you go back to the HomeScreen.tsx file, you will see the red squiggly gone.

Conclusion

🔗

In this tutorial, we discussed how to add type checks to the app screens and how to add type checks to the React Navigation navigators. Using type checks and annotating navigators is a great way to make your app more robust and maintainable when using TypeScript with React Navigation.

I recommend you to check the complete type checking with TypeScript official documentation here provided by React Navigation library maintainers.


More Posts

Browse all posts

Aman Mittal author

I'm a software developer and a technical writer. In this blog, I write about Technical writing, Node.js, React Native and Expo.

Currently, working at Expo. Previously, I've worked as a Developer Advocate, and Senior Content Developer with companies like Draftbit, Vercel and Crowdbotics.


Copyright ©  2019-2022 Aman Mittal · All Rights Reserved.