July 11, 2024
Introduction to React Native Navigation with TypeScript
INTRODUCTION
Navigation is the ability to move from screen to screen within an application. In this post, we will learn how to use React Native's provided APIs to switch and move between different screens, add tabs to screens, and combine different navigation patterns.
Setting Up React Native with TypeScript
First, install React Native with TypeScript:
npx create-expo-app --template
Make sure to select the blank TypeScript template.
After installation, install React Navigation:
npm install @react-navigation/native @react-navigation/native-stack
Here, we installed the React Navigation stack and drawer, which is a pattern of navigation in React Native.
Understanding Navigation
In a web browser, you can link to different pages using an anchor (<a>
) tag. The URL is pushed to the browser history stack when the user clicks on a link. When the user presses the back button, the browser pops the item from the top of the history stack, so the active page is now the previously visited page.
React Native doesn't have a built-in idea of a global history stack like a web browser does -- this is where React Navigation enters the story. React Navigation's native stack navigator allows your app to transition between screens and manage navigation history.
Starting the Project
To start the project, run:
npm start
Working with Stack Navigator
First, create a screens
directory and then create a Home.tsx
file:
import React from 'react';
import { Text, View } from 'react-native';
type Props = {};
function Home({}: Props) {
return (
<View>
<Text>Home Screen</Text>
</View>
);
}
export default Home;
Don't forget to add your own styles. Next, move to your App.tsx
file. The React Native stack exposes a method called createNativeStackNavigator
that returns an object containing two properties: the Screen
and Navigator
properties. These are both React Native components used for configuring the navigator. The Navigator
should contain the Screen
component as its children, and the Screen
component is used to define the application routes (the different screens).
The NavigatorContainer
is a component that manages our navigation tree and contains the navigation state. The NavigatorContainer
component must wrap all navigators.
import { View, Text } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from "@react-navigation/native-stack";
export type RootStackParamList = {
Home: undefined;
};
function HomeScreen() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home Screen</Text>
</View>
);
}
const Stack = createNativeStackNavigator<RootStackParamList>();
function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={HomeScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
export default App;
The RootStackParamList
type defines the route names in the stack as keys and the prop types for the components as their values. The Home
route is typed as undefined
because no props are expected to be passed to the component. If you check your app, you can see a bar with the name "Home". The styles you see for the navigation bar and the content area are the default configuration for a stack navigator. We'll learn how to configure those later.
Adding Another Screen
Create another screen called Details.tsx
in your screens
directory:
import React from "react";
import { StyleSheet, Text, View } from "react-native";
type Props = {};
function Details({}: Props) {
return (
<View style={styles.container}>
<Text>Details Screen</Text>
</View>
);
}
export default Details;
const styles = StyleSheet.create({
container: {
justifyContent: "center",
alignItems: "center",
flex: 1,
},
});
Update your App.tsx
file:
import { StatusBar } from "expo-status-bar";
import { StyleSheet } from "react-native";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import Home from "./screens/Home";
import Details from "./screens/Details";
export type RootStackParamList = {
Home: undefined;
Details: undefined;
};
const Stack = createNativeStackNavigator<RootStackParamList>();
export default function App() {
return (
<>
<StatusBar />
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="Details" component={Details} />
</Stack.Navigator>
</NavigationContainer>
</>
);
}
const styles = StyleSheet.create({});
Route Component Props
All of the route configuration is specified as props to our navigator. The Screen
component accepts a name
prop, which is the name of the route and will also be used to navigate, and a component
prop, which is the component the route will render. You can change the initial route's name on the Navigator to Details
and reload the app.
The Screen
component also accepts an options
prop where you can add more configuration to the screen:
export default function App() {
return (
<>
<StatusBar />
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
name="Home"
component={Home}
options={{ headerShown: false }}
/>
<Stack.Screen
name="Details"
component={Details}
options={{ title: "My Details" }}
/>
</Stack.Navigator>
</NavigationContainer>
</>
);
}
Here, we removed the header in the Home screen and changed the title of the Details screen. You can look at the docs for more possible options https://reactnavigation.org/docs/screen-options/.
Moving Between Screens
Now that we have our navigation set up, let's learn how to move between screens.
Each route component automatically has the navigation
and route
objects passed to them as props. We can use the navigation
object to navigate to different screens:
import React from 'react';
import { View, Text, Button } from 'react-native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { RootStackParamList } from '../App';
type Props = NativeStackScreenProps<RootStackParamList, 'Home'>;
function Home({ navigation }: Props) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Home Screen</Text>
<Button
title="Move To Details Page"
onPress={() => navigation.navigate('Details')}
/>
</View>
);
}
export default Home;
Now, when the button is clicked, we call the navigate
function in the navigation
object to navigate to the Details page. React Native provides a button to navigate back to the Home screen.
Passing Parameters to Routes
We can achieve this by adding parameters in the navigate
method as an object:
function Home({ navigation }: Props) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Home Screen</Text>
<Button
title="Move To Details Page"
onPress={() => navigation.navigate('Details', { detailId: 'One' })}
/>
</View>
);
}
We need to update the RootStackParamList
type in our App.tsx
` so that TypeScript is aware that the Details component is expecting a detailId
param:
export type RootStackParamList = {
Home: undefined;
Details: { detailId: string };
};
Now, in our Details component:
import React from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { RootStackParamList } from '../App';
type Props = NativeStackScreenProps<RootStackParamList, 'Details'>;
function Details({ route }: Props) {
const { detailId } = route.params;
return (
<View style={styles.container}>
<Text>Details Screen</Text>
<Text>{detailId}</Text>
</View>
);
}
export default Details;
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
We can also set initial parameters for our components if the params are not passed in the navigation:
export type RootStackParamList = {
Home: undefined;
Details: { detailId: string; optional?: string };
};
const Stack = createNativeStackNavigator<RootStackParamList>();
export default function App() {
return (
<>
<StatusBar />
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
name="Home"
component={Home}
options={{ headerShown: false }}
/>
<Stack.Screen
name="Details"
component={Details}
options={{ title: 'My Details' }}
initialParams={{ optional: 'This was Passed from initial param' }}
/>
</Stack.Navigator>
</NavigationContainer>
</>
);
}
In Details.tsx
:
function Details({ route }: Props) {
const { detailId, optional } = route.params;
return (
<View style={styles.container}>
<Text>Details Screen</Text>
<Text>{detailId}</Text>
<Text>{optional}</Text>
</View>
);
}
Updating Params
You can also update the screens
parameters by calling the setParams
method in the navigation. Add a button to the Details screen to update the optional
param:
function Details({ navigation, route }: Props) {
const { detailId, optional } = route.params;
return (
<View style={styles.container}>
<Text>Details Screen</Text>
<Text>{detailId}</Text>
<Text>{optional}</Text>
<Button
title="Update Param"
onPress={() => navigation.setParams({ optional: 'Updated' })}
/>
</View>
);
}
Setting Screen Options Dynamically
You can set the screen options using the setOptions
method in the navigation object. Use a useLayoutEffect
hook to update the title of the screen on load:
import React, { useLayoutEffect } from 'react';
function Details({ navigation, route }: Props) {
const { detailId, optional } = route.params;
useLayoutEffect(() => {
navigation.setOptions({ title: `Details: ${detailId}` });
}, [detailId, navigation]);
return (
<View style={styles.container}>
<Text>Details Screen</Text>
<Text>{detailId}</Text>
<Text>{optional}</Text>
<Button
title="Update Param"
onPress={() => navigation.setParams({ optional: 'Updated' })}
/>
</View>
);
}
Nesting Navigators
Nesting navigators means rendering a navigator inside a screen of another navigator. This is similar to nested routes in React Router DOM.
import React from "react";
import { RootStackParamList } from "../App";
import {
createNativeStackNavigator,
NativeStackScreenProps,
} from "@react-navigation/native-stack";
import Feed from "./Feed";
import Messages from "./Messages";
type HomeStackParamList = {
Feed: undefined;
Messages: undefined;
};
type Props = NativeStackScreenProps<RootStackParamList, "Home">;
const Tab = createNativeStackNavigator<HomeStackParamList>();
function Home({}: Props) {
return (
<Tab.Navigator initialRouteName="Feed">
<Tab.Screen name="Feed" component={Feed} />
<Tab.Screen name="Messages" component={Messages} />
</Tab.Navigator>
);
}
export default Home;
In the next post, we will learn how to use the drawer navigation.
471 views