Routing with Expo Router
What is Expo Router?
Expo Router is a library that introduces file-based routing to React Native. With Expo Router, you can organize your application’s navigation structure quickly and efficiently. This means that creating new screens and navigating between them has never been easier.
What we will be creating
We will be building an app with two tabs. The first tab shows a list of cat breeds from this API, and the second tab shows a list of dog breeds from this API. When a user taps on a breed, they will be navigated to the details screen with more information about the breed. We will use Expo Router to handle all of the navigation between screens and tabs. Also, we’ll be using typescript.
Setup Expo Project
To set up an Expo app, run the command below:
npx create-expo-app --template
Choose a blank typescript template and give it any name you like.
When it’s done installing the dependencies, navigate to the project folder and start the server.
cd catXdog
npm start
Now you have a basic Expo project set up, and you’re ready to start using Expo Router to handle navigation in your app.
Installing and Setting Up Expo Router
Run the following to install expo router and its peer dependencies.
npx expo install expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar
Delete the App.tsx
file. Create a new file, index.js
in the root folder and add the following
import "expo-router/entry";
Open package.json
and remove the main
property and its value or replace it with
{
"main": "index.js"
}
Finally, open babel.config.js
and replace it with the code below:
module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
plugins: [require.resolve("expo-router/babel")],
};
};
Start the server with
npm start -- -c
We are ready to start creating our navigation structure using Expo Router. Let’s dive in and create our first screen and tab.
Creating our First Screen
Expo Router uses the app
folder to define your app's navigation structure. So, create a folder called app
in the root directory. Inside the app
directory, create a file index.tsx
. This file will serve as the initial route in the directory as well as any other index.tsx
file in a directory. For this index.tsx
file inside the app
folder will be the welcome screen by default. Now populate the file will the code below:
import { useRouter } from "expo-router";
import { StyleSheet, Text, View } from "react-native";
const WelcomeScreen = () => {
const navigation = useRouter();
return (
<View style={styles.container}>
<Text style={styles.title}>Cat X Dog</Text>
<Text style={styles.subtitle}>Welcome</Text>
<Button title="Cats tab" onPress={() => navigation.push("/cats")} />
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
backgroundColor: "#fff",
},
title: {
fontSize: 24,
fontWeight: "bold",
marginBottom: 10,
},
subtitle: {
fontSize: 16,
textAlign: "center",
},
});
export default WelcomeScreen;
Notice we imported useRouter
from expo-router. This hook allows us to navigate between screens imperatively. The cats screen does not exist yet.
Next, we create another file _layout.tsx
which contains a template for the layout of your navigator and also responsible for rendering elements like the header. Create this file in the app
directory and add the code below:
import { Stack } from 'expo-router';
export default function HomeLayout() {
return (
<Stack />
)
}
We can configure the screen header bar with the screenOptions
props:
import { Stack } from "expo-router";
export default function HomeLayout() {
return (
<Stack
screenOptions={{
headerStyle: {
backgroundColor: "blue",
},
headerTintColor: "white",
headerTitleStyle: {
fontWeight: "bold",
},
}}
/>
);
}
We can change the title of the header of each screen in the navigator using the Screen
component options
prop:
import { Stack } from "expo-router";
export default function HomeLayout() {
return (
<Stack
screenOptions={{
headerStyle: {
backgroundColor: "blue",
},
headerTintColor: "white",
headerTitleStyle: {
fontWeight: "bold",
},
}}
>
<Stack.Screen
name="index"
options={{
title: "Welcome",
}}
/>
</Stack>
);
}
or configure the screen options dynamically using the layout’s Screen component. Open the index.tsx
file and edit it as below:
import { Stack, useRouter } from "expo-router";
import { StyleSheet, Text, View } from "react-native";
const WelcomeScreen = () => {
const navigation = useRouter();
return (
<>
<Stack.Screen
options={{
title: "Welcome",
}}
/>
<View style={styles.container}>
<Text style={styles.title}>Cat X Dog</Text>
<Text style={styles.subtitle}>Welcome</Text>
</View>
</>
);
};
const styles = StyleSheet.create({...});
export default WelcomeScreen;
Setup Tab Navigator
We are going to have two tabs as said earlier.
In Expo Router, folder names that contain parentheses are used to indicate that the directory is a “group” and also when you want to add a layout without adding additional segments to the URL.
In the app
directory, create a folder named (tabs)
. Inside this folder, create a new file called _layout.tsx
and add the following code:
import { Tabs } from "expo-router";
import { Text } from "react-native";
export default function AppLayout() {
return (
<Tabs>
<Tabs.Screen
name="cats"
options={{
title: "Cats",
tabBarIcon: () => <Text>🐱</Text>,
}}
/>
<Tabs.Screen
name="dogs"
options={{
title: "Dogs",
tabBarIcon: () => <Text>🐶</Text>,
}}
/>
</Tabs>
);
}
Let’s create the file for each tab. In (tabs)
directory, create a folder cats
with a file index.tsx
. The name of the folder has to be the same as the name
props specified in the tab layout above.
We could have just created a file named cats.tsx
instead of a folder but we’ll have more than one screen in the stack, a second screen to show the cat details.
Add the following code to the index.tsx
:
import { Link, Stack } from "expo-router";
import { useEffect, useState } from "react";
import { FlatList, Pressable, StyleSheet, Text, View } from "react-native";
const Cats = () => {
const [cats, setCats] = useState([]);
useEffect(() => {
fetch("https://api.thecatapi.com/v1/breeds?limit=20")
.then((response) => response.json())
.then((json) => {
setCats(json);
})
.catch((error) => console.error(error));
}, []);
const renderItem = ({ item }: { item: any }) => (
<Link href={`/cats/${item.id}`} asChild>
<Pressable style={styles.itemContainer}>
<View style={styles.textContainer}>
<Text style={styles.nameText}>{item.name}</Text>
</View>
</Pressable>
</Link>
);
return (
<View>
<Stack.Screen options={{ title: "Dogs" }} />
<FlatList
data={cats}
keyExtractor={({ id }) => id}
renderItem={renderItem}
/>
</View>
);
};
export default Cats;
const styles = StyleSheet.create({
itemContainer: {
flex: 1,
flexDirection: "row",
alignItems: "center",
padding: 16,
borderBottomWidth: 1,
borderBottomColor: "#eee",
},
textContainer: {
marginLeft: 16,
},
nameText: {
fontSize: 16,
fontWeight: "bold",
},
});
We are fetching data from the cat API and rendering the name of each cat.
We are using the Link component, which allows us to navigate between pages and pass the href
prop to the Link component. The href
prop accepts the path to navigate to, the cat's details page. Also, notice the asChild
prop to the Link component. This prop is needed to wrap the Link component around another component.
When you click on a cat item, it should navigate to the page not found because we are yet to create the screen.
Similarly, create a folder dogs
in the (tabs)
directory with a file index.tsx
. Add the following code:
import { Link, Stack } from "expo-router";
import { useEffect, useState } from "react";
import { FlatList, Pressable, StyleSheet, Text, View } from "react-native";
const Dogs = () => {
const [dogs, setDogs] = useState([]);
useEffect(() => {
fetch("https://api.thedogapi.com/v1/breeds?limit=20")
.then((response) => response.json())
.then((json) => {
setDogs(json);
})
.catch((error) => console.error(error));
}, []);
const renderItem = ({ item }: { item: any }) => (
<Link href={`/dogs/${item.id}`} asChild>
<Pressable style={styles.itemContainer}>
<View style={styles.textContainer}>
<Text style={styles.nameText}>{item.name}</Text>
</View>
</Pressable>
</Link>
);
return (
<View>
<Stack.Screen options={{ title: "Dogs" }} />
<FlatList
data={dogs}
keyExtractor={({ id }) => id}
renderItem={renderItem}
/>
</View>
);
};
export default Dogs;
const styles = StyleSheet.create({
itemContainer: {
flex: 1,
flexDirection: "row",
alignItems: "center",
padding: 16,
borderBottomWidth: 1,
borderBottomColor: "#eee",
},
textContainer: {
marginLeft: 16,
},
nameText: {
fontSize: 16,
fontWeight: "bold",
},
});
For both app/cats
and app/dogs
, create a file, _layout.tsx
and add the following code:
import { Stack } from "expo-router";
const Layout = () => {
return <Stack />;
};
export default Layout;
Your app
folder should look like this:
Setting Initial Screen
Before we continue, let’s make the tab the initial screen when you load the app. We’ll make use of the Redirect
component from expo-router.
Open the root index file, app/index.tsx
and replace it with the following code:
import { Redirect } from "expo-router";
const Index = () => {
return <Redirect href="/cats" />;
};
export default Index;
Cat Details Screen with Dynamic Route
Create a file [id].tsx
inside the cats
directory, app/cats[id].tsx
. In Expo Router, a file with a square bracket in its name is a dynamic route. [id].tsx
is a dynamic route with param id
.
Add the following code to the newly created [id].tsx
file:
import { Stack, useSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { StyleSheet, Text, View } from "react-native";
const CatDetails = () => {
const [cat, setCat] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true);
const { id } = useSearchParams();
useEffect(() => {
fetch(`https://api.thecatapi.com/v1/breeds/${id}`)
.then((response) => response.json())
.then((json) => {
setCat(json);
setIsLoading(false);
})
.catch((error) => console.error(error));
}, []);
if (isLoading) {
return <Text>Loading...</Text>;
}
return (
<View>
<Stack.Screen
options={{
title: cat.name,
}}
/>
<View>
<Text style={styles.name}>Name: {cat.name}</Text>
<Text style={styles.text}>Origin: {cat.origin}</Text>
<Text style={styles.text}>Temperament: {cat.temperament}</Text>
<Text style={styles.text}>Description: {cat.description}</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
name: {
fontSize: 20,
fontWeight: "bold",
textAlign: "center",
margin: 10,
},
text: {
fontSize: 16,
textAlign: "center",
margin: 10,
},
});
export default CatDetails;
We are also changing the title in the header based on the cat’s name.
Next, we do the same for the app/dogs
directory. Create a file, [id].tsx
, and the following code to it:
import { Stack, useSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { StyleSheet, Text, View } from "react-native";
const DogDetails = () => {
const [dog, setDog] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true);
const { id } = useSearchParams();
useEffect(() => {
fetch(`https://api.thedogapi.com/v1/breeds/${id}`)
.then((response) => response.json())
.then((data) => {
setDog(data);
setIsLoading(false);
})
.catch((error) => console.error(error));
}, []);
if (isLoading) {
return <Text>Loading...</Text>;
}
return (
<View>
<Stack.Screen
options={{
title: dog.name,
}}
/>
<View>
<Text style={styles.name}>Name: {dog.name}</Text>
<Text style={styles.text}>Origin: {dog.origin}</Text>
<Text style={styles.text}>Temperament: {dog.temperament}</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
name: {
fontSize: 20,
fontWeight: "bold",
textAlign: "center",
margin: 10,
},
text: {
fontSize: 16,
textAlign: "center",
margin: 10,
},
});
export default DogDetails;
And that concludes our app navigation. We can navigate between pages by clicking on a breed.
Testing Deep Links
Expo router takes care of setting up deep linking. The entire deep linking system is automatically generated live making every screen deep linkable, so you can share links to any route in your app.
We need to set up the scheme for deep linking.
Open the app.json
file in your project root and add the following to the expo
key:
"expo": {
"scheme": "catxdog",
"web": {
"bundler": "metro"
}
}
The app.json
file should be similar to this:
{
"expo": {
"name": "catXdog",
"slug": "catXdog",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
}
},
"scheme": "catxdog",
"web": {
"bundler": "metro",
"favicon": "./assets/favicon.png"
}
}
}
We can now open links using uri-scheme
.
In your terminal, run the following and replace 192.168.87.39
with your IP address:
# android
npx uri-scheme open exp://192.168.88.58:19000/--/dogs/2 --android
# ios
npx uri-scheme open exp://192.168.87.39:19000/--/dogs/2 --ios
Using this link will open the details screen for the dog with an id of 2.
Conclusion
Expo Router is a powerful routing library for React Native that allows you to easily navigate between different screens in your app. It provides a simple and intuitive API that makes it easy to set up your navigation stack and add custom transitions and animations.
Overall, Expo Router is a great choice for anyone looking for a flexible and powerful routing library for their React Native app. And also, it is stable which means it can be used in production.
The GitHub repository can be found here.
Level Up Coding
Thanks for being a part of our community! Before you go:
- 👏 Clap for the story and follow the author 👉
- 📰 View more content in the Level Up Coding publication
- 💰 Free coding interview course ⇒ View Course
- 🔔 Follow us: Twitter | LinkedIn | Newsletter
🚀👉 Join the Level Up talent collective and find an amazing job