Getting Started with React Navigation v6 and TypeScript in React Native

Aman Mittal - Jul 1 '22 - - Dev Community

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 becomes easier.

React Navigation library is one of the most used navigation libraries in the 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 the following already installed:

  • 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
Enter fullscreen mode Exit fullscreen mode

After navigating inside the project directory, run the following command to install React Navigation libraries and their 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
Enter fullscreen mode Exit fullscreen mode

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 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:

import { StyleSheet, View, Text, Pressable, FlatList } from 'react-native';
import { useNavigation } from '@react-navigation/native';

const DATA = [
  {
    id: 1,
    name: 'Luke Skywalker',
    birth_year: '19BBY',
  },
  {
    id: 2,
    name: 'C-3PO',
    birth_year: '112BBY',
  },
  {
    id: 3,
    name: 'R2-D2',
    birth_year: '33BBY',
  },
  {
    id: 4,
    name: 'Darth Vader',
    birth_year: '41.9BBY',
  },
  {
    id: 5,
    name: 'Leia Organa',
    birth_year: '19BBY',
  },
];

const HomeScreen = () => {
  const navigation = useNavigation();

  const renderListItems = ({ item }) => {
    return (
      <Pressable
        onPress={() =>
          navigation.navigate('Details', {
            name: item.name,
            birthYear: item.birth_year,
          })
        }
      >
        <Text
          style={{ fontSize: 18, paddingHorizontal: 12, paddingVertical: 12 }}
        >
          {item.name}
        </Text>
        <View
          style={{
            borderWidth: StyleSheet.hairlineWidth,
            borderColor: '#ccc',
          }}
        />
      </Pressable>
    );
  };

  return (
    <View style={{ flex: 1, paddingTop: 10 }}>
      <FlatList data={DATA} renderItem={renderListItems} />
    </View>
  );
};

export default HomeScreen;
Enter fullscreen mode Exit fullscreen mode

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:

import { View, Text } from 'react-native';
import { useRoute } from '@react-navigation/native';

const DetailScreen = () => {
  const route = useRoute();
  const { name, birthYear } = route.params;

  return (
    <View style={{ flex: 1, paddingTop: 12, paddingHorizontal: 10 }}>
      <Text style={{ fontSize: 18, paddingBottom: 12 }}>Name: {name}</Text>
      <Text style={{ fontSize: 18 }}>Birth Year: {birthYear}</Text>
    </View>
  );
};

export default DetailScreen;
Enter fullscreen mode Exit fullscreen mode

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:

import * as React from 'react';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

import HomeScreen from '../screens/HomeScreen';
import DetailsScreen from '../screens/DetailsScreen';

const HomeStack = createNativeStackNavigator();

const HomeStackNavigator = () => {
  return (
    <HomeStack.Navigator>
      <HomeStack.Screen name="Home" component={HomeScreen} />
      <HomeStack.Screen name="Details" component={DetailsScreen} />
    </HomeStack.Navigator>
  );
};

export default HomeStackNavigator;
Enter fullscreen mode Exit fullscreen mode

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:

import * as React from 'react';
import { NavigationContainer } from '@react-navigation/native';

import HomeStackNavigator from './HomeStack';

const RootNavigator = () => {
  return (
    <NavigationContainer>
      <HomeStackNavigator />
    </NavigationContainer>
  );
};

export default RootNavigator;
Enter fullscreen mode Exit fullscreen mode

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

import { StatusBar } from 'expo-status-bar';

import RootNavigator from './src/navigation';

export default function App() {
  return (
    <>
      <RootNavigator />
      <StatusBar style="auto" />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

export type HomeStackNavigatorParamList = {
  Home: undefined;
  Details: {
    name: string;
    birthYear: string;
  };
};
Enter fullscreen mode Exit fullscreen mode

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.

// rest of the import statements remain same
import { HomeStackNavigatorParamList } from './types';

const HomeStack = createNativeStackNavigator<HomeStackNavigatorParamList>();

// rest of the code remains the same
Enter fullscreen mode Exit fullscreen mode

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.

<HomeStack.Screen name="Detail" component={DetailsScreen} />
Enter fullscreen mode Exit fullscreen mode

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 generic type. For example, NativeStackNavigationProp is used for @react-navigation/native-stack. Import that in the types.ts file.

import type { NativeStackNavigationProp } from '@react-navigation/native-stack';

export type HomeStackNavigatorParamList = {
  Home: undefined;
  Details: {
    name: string;
    birthYear: string;
  };
};

export type HomeScreenNavigationProp = NativeStackNavigationProp<
  HomeStackNavigatorParamList,
  'Details'
>;
Enter fullscreen mode Exit fullscreen mode

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 parameter, 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.

// after other import statements

// import HomeScreenNavigationProp
import { HomeScreenNavigationProp } from '../navigation/types';

// inside the HomeScreen, component modify the following line
const HomeScreen = () => {
  const navigation = useNavigation<HomeScreenNavigationProp>();

  // rest of the code remains the same
};
Enter fullscreen mode Exit fullscreen mode

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

function HomeScreen({ navigation }: HomeScreenNavigationProp) {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

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.

// after other import statements
import type { RouteProp } from '@react-navigation/native';

export type DetailsScreenRouteProp = RouteProp<
  HomeStackNavigatorParamList,
  'Details'
>;

// rest of the code remains the same
Enter fullscreen mode Exit fullscreen mode

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

// after other import statements
import { DetailsScreenRouteProp } from '../navigation/types';

// inside the DetailsScreen, the component modify the following line

const DetailScreen = () => {
  const route = useRoute<DetailsScreenRouteProp>();

  // rest of the code remains the same
};
Enter fullscreen mode Exit fullscreen mode

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:

import { View, Text } from 'react-native';

const FeedScreen = () => {
  return (
    <View style={{ flex: 1, paddingTop: 12, paddingHorizontal: 10 }}>
      <Text style={{ fontSize: 18 }}>Feed Screen</Text>
    </View>
  );
};

export default FeedScreen;
Enter fullscreen mode Exit fullscreen mode

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

import { View, Text } from 'react-native';

const SettingsScreen = () => {
  return (
    <View style={{ flex: 1, paddingTop: 12, paddingHorizontal: 10 }}>
      <Text style={{ fontSize: 18 }}>Settings Screen</Text>
    </View>
  );
};

export default SettingsScreen;
Enter fullscreen mode Exit fullscreen mode

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.

export type BottomTabNavigatorParamList = {
  Home: HomeStackNavigatorParamList;
  Feed: undefined;
  Settings: undefined;
};
Enter fullscreen mode Exit fullscreen mode

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

import * as React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

import { BottomTabNavigatorParamList } from './types';
import HomeStackNavigator from './HomeStack';
import FeedScreen from '../screens/FeedScreen';
import SettingsScreen from '../screens/SettingsScreen';

const Tab = createBottomTabNavigator<BottomTabNavigatorParamList>();

const BottomTabs = () => {
  return (
    <Tab.Navigator>
      <Tab.Screen
        name="HomeStack"
        component={HomeStackNavigator}
        options={{ headerShown: false }}
      />
      <Tab.Screen name="Feed" component={FeedScreen} />
      <Tab.Screen name="Settings" component={SettingsScreen} />
    </Tab.Navigator>
  );
};

export default BottomTabs;
Enter fullscreen mode Exit fullscreen mode

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.

// rest of the import statements remain same
import BottomTabs from './Tabs';

const RootNavigator = () => {
  return (
    <NavigationContainer>
      <BottomTabs />
    </NavigationContainer>
  );
};

export default RootNavigator;
Enter fullscreen mode Exit fullscreen mode

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:

return (
  <View style={{ flex: 1, paddingTop: 10 }}>
    <Pressable
      onPress={() => navigation.navigate('Feed')}
      style={{
        padding: 8,
        borderWidth: 1,
        borderRadius: 4,
        borderColor: 'red',
        margin: 12,
        alignItems: 'center',
      }}
    >
      <Text style={{ fontSize: 16, fontWeight: '600' }}>Go to Feed screen</Text>
    </Pressable>
    <FlatList data={DATA} renderItem={renderListItems} />
  </View>
);
Enter fullscreen mode Exit fullscreen mode

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:

import type {
  CompositeNavigationProp,
  RouteProp,
} from '@react-navigation/native';
// rest of the import statements remains same

export type HomeScreenNavigationProp = CompositeNavigationProp<
  NativeStackNavigationProp<HomeStackNavigatorParamList, 'Details'>,
  BottomTabNavigationProp<BottomTabNavigatorParamList, 'Feed'>
>;

// rest of the code remains the same
Enter fullscreen mode Exit fullscreen mode

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 check the complete type checking with TypeScript official documentation here provided by React Navigation library maintainers.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .