Routing with Expo Router

Joseph Odunsi
Level Up Coding
Published in
9 min readMar 29, 2023

--

Photo by Javier Allegue Barros on Unsplash

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:

🚀👉 Join the Level Up talent collective and find an amazing job

--

--