Skip to content

Getting Started with Stack Navigator using react-navigation 5 in React Native and Expo apps

Published:

15 min read

React Navigation as the library released its 5th stable version recently, just being two years old. Without a doubt, it is one of the most popular navigation solutions in React Native apps that also has support for the Expo.

Recently, it underwent some core changes and how you used to define routes till react-navigation version 4.x.x has some major changes as to how you are going to define routes using the latest version of the library.

Some of the major highlights the team of maintainers released in a blog post are that the navigation patterns are now more component-based, common use cases can now be handled with pre-defined Hooks, new architecture allows you to configure and update a screen from the component itself and some other changes that you can dwell in the blog post here.

The major highlight from these new changes in the component based configuration. If you have experience developing with web development libraries such as Reactjs in combination with react-router, you won’t experience much of a learning curve here. However, if you are diving into React Native recently, and chances are that you are going to use react-navigation, I’d recommend starting with this latest version. I hope this tutorial serves as a starting point or a refreshing one in your journey.

If you’d like to receive more React Native tutorials in your inbox, you can sign up for my newsletter here.

Table of contents

Open Table of contents

Requirements

Requirements for this tutorial is simple. Have the following installed on your local dev environment.

Do note that, without dwelling much into the configuration of native binaries with the react-navigation library, I am going to expo-cli to generate the project and Expo client to view output from time to time. Make sure you have both installed.

Install dependencies

To start, generate a new Expo project with a blank template by running the following command in a terminal window.

npx expo init [Project Name]

# after the project directory has been generated

cd [Project Name]

Next, install the following dependencies for the react-navigation library to work. The first command is going to install core utilities of react-navigation that are used by navigators to create the navigation structure in the app. The second command uses expo install instead of npm install or yarn add. The reason is that expo is going to install the version of the libraries mentioned that are compatible with the Expo SDK.

yarn add @react-navigation/native @react-navigation/stack

# use expo install for Expo projects only
expo install react-native-gesture-handler react-native-reanimated
react-native-screens react-native-safe-area-context
@react-native-community/masked-view

Do note that the package @react-navigation/stack is only required to install when you are going to use the Stack navigation pattern in the app. For example, if you are just going to use tab navigation, you are going to install a different package as shown here.

Create mock screens

Create a new directory src/screens and inside it, create two new files called Home.js and Detail.js. The code snippet for both of these files is listed below:

// src/screens/Home.js

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

function Home() {
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Home Screen</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#ebebeb'
  },
  text: {
    color: '#101010',
    fontSize: 24,
    fontWeight: 'bold'
  }
});

export default Home;

// src/screens/Detail.js

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

function Details() {
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Details Screen</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#ebebeb'
  },
  text: {
    color: '#101010',
    fontSize: 24,
    fontWeight: 'bold'
  }
});

export default Details;

These screen components are for demonstration purposes. You have to feed the routes to the navigator to work with, these screen components are going to the routes.

Create a basic stack navigator

In this section, let us set up a basic Stack navigator. Start by creating a new directory src/navigation. The best definition of what a Stack Navigator does can be read from its docs. I am going to quote it here:

React Navigation’s stack navigator provides a way for your app to transition between screens and manage navigation history. If your app uses only one stack navigator then it is conceptually similar to how a web browser handles navigation state - your app pushes and pops items from the navigation stack as users interact with it, and this results in the user seeing different screens.

Now that you have an idea of what exactly a stack navigation pattern is, let us start by creating one. Inside the src/navigation directory, create a new file called MainStackNavigator.js and import the following statements:

import * as React from "react";
import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";

import Home from "../screens/Home";

From the above snippet, the NavigationContainer is a component that manages the navigation tree. It also contains the navigation state and it has to wrap all navigators structure.

The createStackNavigator is a function that is used to implement a stack navigation pattern. This function returns two React components: Screen and Navigator that help to configure each component screen. For now, let us add one screen to this navigation pattern.

const Stack = createStackNavigator();

function MainStackNavigator() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={Home} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

export default MainStackNavigator;

In the above snippet, there are two required props with each Stack.Screen. The prop name refers to the name of the route and prop component specifies which screen to render at the particular route.

Do not forget to export the MainStackNavigator since it is going to be imported in the root of the app, that is, inside App.js file as shown below.

import React from "react";

import MainStackNavigator from "./src/navigation/MainStackNavigator";

export default function App() {
  return <MainStackNavigator />;
}

Execute the command expo start and make sure the Expo client is running either in a simulator device or a real device. You are going to get the HomeScreen as the following result.

Specifying options for each screen in Stack Navigator

By default, it shows the title bar on the screen. However, you can set the title of the screen. Let us change the title of the screen shown, from Home to render Home Screen.

This is done by specifying the options on each screen as shown below. Open MainStackNavigator.js file and the prop options on Stack.Screen for Home component.

<Stack.Screen name="Home" component={Home} options={{ title: "Home Screen" }} />

The changes are instantly reflected in the Expo client.

In the current stack navigator structure, let us add the second screen component called Detail. Import it from screens/Detail.js and another route as shown below.

// rest import statements remain same
import Detail from "../screens/Detail";

function MainStackNavigator() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen
          name="Home"
          component={Home}
          options={{ title: "Home Screen" }}
        />
        <Stack.Screen
          name="Detail"
          component={Detail}
          options={{ title: "Detail Screen" }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

To see that Detail screen is currently in our stack, try adding the prop initialRouteName on Stack.Navigator. The first screen that renders is going to be the Detail screen.

<Stack.Navigator initialRouteName='Detail'>

Here is the output:

But we need a way to navigate from the Home screen to the Detail screen not just display the later screen as the initial route. Change the value of initialRouteName to Home.

<Stack.Navigator initialRouteName='Home'>

Then, open screen/Home.js and a button component that is going to navigate to the Detail screen when pressed.

Import TouchableOpacity from react-native core and make sure to utilize the navigation prop passed to the Home screen. This prop is passed to every screen that is a route wrapped by the Stack Navigator.

import React from "react";
import { StyleSheet, View, Text, TouchableOpacity } from "react-native";

function Home(props) {
  const { navigation } = props;
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Home Screen</Text>
      <TouchableOpacity
        style={styles.buttonContainer}
        onPress={() => navigation.navigate("Detail")}
      >
        <Text style={styles.buttonText}>Go to Detail Screen</Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "#ebebeb",
  },
  text: {
    color: "#101010",
    fontSize: 24,
    fontWeight: "bold",
  },
  buttonContainer: {
    backgroundColor: "#222",
    borderRadius: 5,
    padding: 10,
    margin: 20,
  },
  buttonText: {
    fontSize: 20,
    color: "#fff",
  },
});

export default Home;

Here is the changed Home screen:

Enabling gestures in react-navigation

If you press the button, you are going to notice that it navigates you to the Detail screen. On the Detail screen, do note that the back button with the name of the previous screen is shown in the header.

The above demo is how the navigation between two-screen works on an iOS device. The default native transition on iOS when using stack navigation is that the screen is pushed or pulled from the right side. On Android, as you will notice below, the behavior is different. The new screen is pushed from the bottom.

Also, in the below demo notice that on iOS swipe gesture works when going back from Detail to Home screen. On Android, it doesn’t.

To enable gestures on Android as well, in Stack.Navigator you have to add a prop called screenOptions. This prop is used when you want to pass some value to all the children’s routes of a stack navigator.

<Stack.Navigator
        initialRouteName='Home'
        screenOptions={{
          gestureEnabled: true
        }}>

This is going to enable the gestures on Android as well.

Passing data between routes

You can pass parameters to a route by putting the params in an object as the second argument using navigation.navigate. Let’s mimic a small example by passing data from Home to Detail screen.

Add the following mock object for some data in Home.js.

const character = {
  name: "Luke Skywalker",
  home: "Tatooine",
  species: "human",
};

Then, in the same screen component file, modify the TouchableOpacity and pass the previous object as the second argument.

<TouchableOpacity
  style={styles.buttonContainer}
  onPress={() => navigation.navigate("Detail", { item: character })}
>
  <Text style={styles.buttonText}>Who is {character.name}?</Text>
</TouchableOpacity>

Here is the output:

Open Detail.js and add the following code snippet. Using route.params this screen component can read the parameters passed from the Home screen. Inside the Detail component, let us destructure the route.params and then display those values.

import React from "react";
import { StyleSheet, View, Text } from "react-native";

function Detail(props) {
  const { route } = props;
  const { item } = route.params;
  const { name, home, species } = item;
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Detail Screen</Text>
      <View style={styles.card}>
        <Text style={styles.cardText}>Name: {name}</Text>
        <Text style={styles.cardText}>Home Planet: {home}</Text>
        <Text style={styles.cardText}>Species: {species}</Text>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "#ebebeb",
  },
  text: {
    color: "#101010",
    fontSize: 24,
    fontWeight: "bold",
  },
  card: {
    width: 350,
    height: 100,
    borderRadius: 10,
    backgroundColor: "#101010",
    margin: 10,
    padding: 10,
    alignItems: "center",
  },
  cardText: {
    fontSize: 18,
    color: "#ffd700",
    marginBottom: 5,
  },
});

export default Detail;

Here is the output showing above works:

How to use params in the screen’s title

You can use params in the title of the screen component. For example, instead of saying Detail Screen, it could say the name of the character.

This can be done by passing route as an object in options for Detail screen in MainStackNavigator.js file and use the value of the title from route.params.item.name.

<Stack.Screen
  name="Detail"
  component={Detail}
  options={({ route }) => ({
    title: route.params.item.name,
  })}
/>

Here is the output:

Using common screenOptions to modify header styles

You can use the prop screenOptions to apply common styles to the header across the navigator. For example, in the code snippet, below, let us set two properties, headerStyle, headerTintColor and headerTitleStyle to change the background color of all screen headers as well as the color of the title on each screen.

<Stack.Navigator
  initialRouteName="Home"
  screenOptions={{
    gestureEnabled: true,
    headerStyle: {
      backgroundColor: "#101010",
    },
    headerTitleStyle: {
      fontWeight: "bold",
    },
    headerTintColor: "#ffd700",
  }}
>
  {/* ... */}
</Stack.Navigator>

The headerStyle is a style object that can be used to set the backgroundColor of the header for the screen component.

The headerTitleStyle is another style object that allows you to customize the title or the text of the header.

The headerTintColor is the color property for both the back button and the title of the header.

Here is the output in action after the above changes:

Making the back button title invisible on iOS

So far, you must have noticed that on iOS the back button shows the name of the previous screen by default. On Android, this behavior is only shown by a back button icon.

To make an iOS app just to show the back button icon instead of the name of the previous screen in the stack, add the following property to screenOptions on Stack.Navigator.

<Stack.Navigator
  initialRouteName="Home"
  screenOptions={{
    gestureEnabled: true,
    headerStyle: {
      backgroundColor: "#101010",
    },
    headerTitleStyle: {
      fontWeight: "bold",
    },
    headerTintColor: "#ffd700",
    headerBackTitleVisible: false,
  }}
>
  {/* ... */}
</Stack.Navigator>

Here is the output:

Understanding header modes and changing it in the Android app

Using the react-navigation library, there are three header modes available that render the header in different ways. By default on iOS, the headerMode is of the value of float.

On Android, the value screen is commonly used. These are the native patterns of how a header renders on each platform. The last header mode value is none which abolishes any header to render.

Take a look at the below demo of how it differs on both platforms.

In the section, let us make the header mode of the Android app behave in the same way as the iOS app. Just add the property headerMode with the value of float in Stack.Navigator.

<Stack.Navigator
  initialRouteName="Home"
  screenOptions={{
    gestureEnabled: true,
    headerStyle: {
      backgroundColor: "#101010",
    },
    headerTitleStyle: {
      fontWeight: "bold",
    },
    headerTintColor: "#ffd700",
    headerBackTitleVisible: false,
  }}
  headerMode="float"
>
  {/* ... */}
</Stack.Navigator>

The header in the Android app, when navigating from one screen to another, is going to stay fixed, just link in the iOS app.

Directly navigating from the third screen to the top of the stack screen navigator

In this section, let us create a small demo on how you can leverage a helper method from navigation prop to navigate back to the top or first screen in the stack navigator from any other screen in navigator’s structure, no matter how deep.

Start by creating a new file called Settings.js inside the src/screens/ directory and the following component snippet.

import React from "react";
import { StyleSheet, View, Text } from "react-native";

function Settings() {
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Settings</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "#ebebeb",
  },
  text: {
    color: "#101010",
    fontSize: 24,
    fontWeight: "bold",
  },
});

export default Settings;

Next, modify the MainStackNavigator.js file and import a new screen.

import Settings from "../screens/Settings";

Add this newly imported screen to the current Stack.Navigator.

<Stack.Screen
  name="Settings"
  component={Settings}
  options={{ title: "Settings" }}
/>

Open, Detail.js and modify it to add a button. When this button is pressed, the navigator leads to the Settings screen.

import React from "react";
import { StyleSheet, View, Text, TouchableOpacity } from "react-native";

function Detail(props) {
  const { route, navigation } = props;
  const { item } = route.params;
  const { name, home, species } = item;
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Detail Screen</Text>
      <View style={styles.card}>
        <Text style={styles.cardText}>Name: {name}</Text>
        <Text style={styles.cardText}>Home Planet: {home}</Text>
        <Text style={styles.cardText}>Species: {species}</Text>
      </View>
      <TouchableOpacity
        style={styles.buttonContainer}
        onPress={() => navigation.navigate("Settings")}
      >
        <Text style={styles.buttonText}>Go to Settings</Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "#ebebeb",
  },
  text: {
    color: "#101010",
    fontSize: 24,
    fontWeight: "bold",
  },
  card: {
    width: 350,
    height: 100,
    borderRadius: 10,
    backgroundColor: "#101010",
    margin: 10,
    padding: 10,
    alignItems: "center",
  },
  cardText: {
    fontSize: 18,
    color: "#ffd700",
    marginBottom: 5,
  },
  buttonContainer: {
    backgroundColor: "#222",
    borderRadius: 5,
    padding: 10,
    margin: 20,
  },
  buttonText: {
    fontSize: 20,
    color: "#fff",
  },
});

export default Detail;

In the following demo, you are going to notice that to move back from Settings screen to Home screen, you have to pass through the Detail screen.

However, using the helper method navigation.popToTop() without any arguments, you can navigate from Settings screen to the Home screen directly.

To accomplish this, modify the Settings.js file as below by adding a button. The onPress of this button is going to make use of the helper method.

import React from "react";
import { StyleSheet, View, Text, TouchableOpacity } from "react-native";

function Settings(props) {
  const { navigation } = props;
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Settings</Text>
      <TouchableOpacity
        style={styles.buttonContainer}
        onPress={() => navigation.popToTop()}
      >
        <Text style={styles.buttonText}>Go to Home</Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "#ebebeb",
  },
  text: {
    color: "#101010",
    fontSize: 24,
    fontWeight: "bold",
  },
  buttonContainer: {
    backgroundColor: "#222",
    borderRadius: 5,
    padding: 10,
    margin: 20,
  },
  buttonText: {
    fontSize: 20,
    color: "#fff",
  },
});

export default Settings;

Here is the demo:

Conclusion

Congratulations! You’ve completed this tutorial.

In this tutorial, we have discussed many strategies and properties that you can apply and implement in your Stack navigator. The main objective is to get familiar with the component-based configuration of the Stack Navigator in the latest version of the react-navigation library.

Here is the link to the complete Stack Navigator API here I’d recommend you to check.

Source code

Originally published at Heartbeat.fritz.ai


Previous Post
Nesting Tab and Stack navigators in React Native and Expo apps
Next Post
How to handle navigation in WebViews in a React Native app

I'm a software developer and technical writer. On this blog, I share my learnings about both fields. Recently, I have begun exploring other topics, so don't be surprised if you find something new here.

Currently, working as a documentation lead at Expo.