Expo is a platform that enables building native iOS and Android apps using React Native, while Expo Router is a file-based router designed for universal React Native apps that can be utilized to incorporate navigation into your application. it makes every file in our app
directory a route. In this article, we will explore how to use Expo router by building a Rick and Morty app that consumes a REST API.
get code on Github: https://github.com/evergreenx/ricknmorty-expo-router
Here is a preview of what we will be building
Prerequisites
Before we dive into the building, there are a few prerequisites you should be familiar with :
Node js v14+.
Basic knowledge of react native : Expo is built on react native, so you should have some understanding of the core concepts of React Native.
Familiarity with JavaScript : Our application will be built using JavaScript, so you should have a basic understanding of the language.
Understanding of REST API : you should have a basic understanding to consume a REST API.
If you are new to any of these concepts, I recommend taking some time to learn them before continuing with this article.
What is Expo Router?
It is a routing concept built on top of React Navigation suite. if you are familiar with how Nextjs handles navigation that should give you a basic understanding of Expo router. that is every file in your app
directory becomes a route in your app.
Let's Get Started
To get started, head over to expo documentation to install Expo cli and Expo Go, if you already don't have it installed. Now you need to create a new expo project with Expo CLI. You can use the following command to create a new expo typescript project:
npx create-react-native-app -t with-typescript ricknmorty-expo-router
Once, it is done installing our project, navigate to the project directory and start the development server by running:
cd ricknmorty-expo-router
npx expo start
This will launch the Expo development server, which you can preview your app in the browser or on your phone using Expo Go.
If all went well you should have a screen preview like this
Install Expo Router
Now let's 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
After successful installation, let's configure our app to use expo router.
Create a new file index.ts
in the root of your project. If it exists already, replace it with the following code :
import "expo-router/entry";
Also, create app.json
file and add this code, we are adding a deep linking scheme to our app.
{
"expo": {
"scheme": "myapp",
"web": {
"bundler": "metro"
}
}
}
Update your package.json
with this code.
// If you use Yarn
"resolutions": {
"metro": "0.76.0",
"metro-resolver": "0.76.0"
}
// if you use npm
"overrides": {
"metro": "0.76.0",
"metro-resolver": "0.76.0"
}
Next, we have to update our babel.config.js
file and replace the content with the code below.
module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
plugins: [require.resolve("expo-router/babel")],
};
};
Let us create a new directory called app
, this is where our routes will live, go ahead and add a new index.tsx
file in your just-created directory. your project should look something like this.
After this add a react function component to index.tsx
and delete App.tsx
import { View, Image, ViewStyle } from "react-native";
import React from "react";
const Index = () => {
return (
<View style={$ViewStyle}>
</View>
);
};
export default Index;
const $ViewStyle: ViewStyle = {
flex: 1,
padding: 20,
backgroundColor: "#FFDEAD",
};
Now restart your server.
npx expo start
Here is a preview of how our app looks
Fetch Data
We have successfully set up our project with Expo Router and index.js
now serves as our home screen. Next, we are going to consume the rick and morty API. For this article, we will be using only the character's resources. To consume the API we are going to make use of Apisauce which is an HTTP client wrapper built on Axios. Let's install the package.
// npm
npm i apisauce
// yarn
yarn add apisauce
Create a interface.ts
file in the root of our application, this is where the interface for our Characters API response will live.
// interface.ts
export interface Location {
name: string;
url: string;
}
export interface Character {
id: number;
name: string;
status: string;
species: string;
type: string;
gender: string;
origin: Location;
location: Location;
image: string;
medium : string;
episode: string[];
url: string;
created: string;
}
export interface ApiResponse {
info: {
count: number;
pages: number;
next: string;
prev: string;
};
results: Character[];
}
Moving forward create a new file called api.ts
and copy this code.
import { create } from "apisauce";
import { ApiResponse} from "./interface";
const api = create({
baseURL: "https://rickandmortyapi.com/api/",
headers: { Accept: "application/json" },
});
export const getCharacters = async (): Promise<ApiResponse> => {
const response = await api.get("/character");
return response.data as ApiResponse;
};
Firstly we are importing create
from apisauce and also importing the interface we created earlier. Next, we are defining the API with the create
method. The getCharacters
function is an async function that makes a GET request to the /characters
endpoint. Open and replace the index.tsx
component with the code below.
import { View, Image, ViewStyle, Text, TextStyle } from "react-native";
import React, { useEffect, useState } from "react";
import { getCharacters } from "../api";
import { Character } from "../interface";
import CharacterCard from "../component/CharacterCard";
const Index = () => {
const [characters, setCharacters] = useState<Character[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
useEffect(() => {
setLoading(true);
const fetchCharactersData = async () => {
try {
const res = await getCharacters();
setCharacters(res.results);
} catch (err) {
setLoading(false);
setCharacters([]);
setError("An error occurred");
} finally {
setLoading(false);
}
};
fetchCharactersData();
}, []);
console.log(characters);
return (
<View style={$container}>
<View>
<Image
source={{
uri: "https://res.cloudinary.com/evergreenx/image/upload/v1680259111/Logo_1_tdttqu.png",
}}
resizeMode="contain"
alt="logo"
style={{ width: 300, height: 200, alignSelf: "center" }}
/>
</View>
{loading && (
<Image
source={{
uri: "https://res.cloudinary.com/evergreenx/image/upload/v1680297056/Loading_component_tr2ec6.png",
}}
resizeMode="contain"
style={{ width: 300, height: 200, alignSelf: "center" }}
/>
)}
{error && <Text style={$errorText}>{error}</Text>}
{characters && <CharacterCard characters={characters} />}
</View>
);
};
export default Index;
const $container: ViewStyle = {
flex: 1,
width: "100%",
backgroundColor: "#FFDEAD",
};
const $errorText: TextStyle = {
color: "red",
fontSize: 20,
alignSelf: "center",
};
We imported our async function and used a useEffect to get our data on-load of the component, The error and data have been saved in a useState. We are also importing CharacterCard
component which does not exist yet ,it will be used to display the data.
Show Characters
Now let's show the data, create a component folder at the root of the project, and add a new file called CharacterCard.tsx
.
import {
View,
Text,
FlatList,
TextStyle,
ViewStyle,
Image,
} from "react-native";
import React from "react";
import { Character } from "../interface";
type CharacterCardProps = {
characters: Character[];
};
export default function characterCard({ characters }: CharacterCardProps) {
const renderCharacters = ({ item }: { item: Character }) => {
return (
<View style={$characterContainer}>
<View style={$characterImageContainer}>
<Image
source={{ uri: item?.image }}
style={{
width: "100%",
height: 150,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
borderRadius: 10,
}}
resizeMode="cover"
/>
</View>
<View style={$characterInfoContainer}>
<Text style={$characterText}>{item?.name}</Text>
<Text style={$characterSpecies}>{item?.species}</Text>
</View>
</View>
);
};
const renderHeader = () => {
return (
<View>
<Image
source={{
uri: "https://res.cloudinary.com/evergreenx/image/upload/v1680259111/Logo_1_tdttqu.png",
}}
style={{ width: 300, height: 200, alignSelf: "center" }}
resizeMode="contain"
alt="logo"
/>
</View>
);
};
return (
<View style={$container}>
<FlatList
style={{ width: "100%", padding: 10 }}
data={characters}
renderItem={renderCharacters}
keyExtractor={(item) => item.id.toString()}
showsHorizontalScrollIndicator={true}
numColumns={2}
ListHeaderComponent={renderHeader}
/>
</View>
);
}
const $container: ViewStyle = {
flex: 1,
alignItems: "center",
justifyContent: "center",
};
const $characterContainer: ViewStyle = {
backgroundColor: "#fff",
height: 230,
width: "45%",
borderRadius: 10,
marginVertical: 10,
marginHorizontal: 10,
};
const $characterImageContainer: ViewStyle = {
width: "100%",
overflow: "hidden",
marginBottom: 10,
};
const $characterText: TextStyle = {
color: "#000",
fontSize: 18,
fontWeight: "500",
};
const $characterSpecies: TextStyle = {
color: "#000",
fontSize: 13,
fontWeight: "500",
marginTop: 5,
};
const $characterInfoContainer: ViewStyle = {
padding: 10,
};
Let's break down what the code does, so we render a grid of cards containing each character, also the component takes a prop characters
which is an array of objects. The component uses a FlatList
to render the cards and the keyExtractor
prop is used to extract a unique key for each item in the array, numcolumns
prop is set to display the cards in a grid with two columns and the showsVerticalScrollIndicator
prop is set to false
to hide the vertical scroll indicator. renderHeader
prop is used to render a component on top of the list, we render an image component with an our image source hosted on Cloudinary.
We define renderCharacters
a function that takes an object with an item property that represents a single character from the characters
array. The function returns a JSX representation of a character card and lastly, and we define some styles to make the cards look good.
So far our app is coming together piece by piece. it should look like this.
Move Between Screens
One should be able to move between screens and we will see how to do that in Expo Router. To achieve this expo router has a <Link
/>
component or useRouter
hook. so let's make it when we click on a character we move to a dynamic route to view that particular character's details.
Head over to the project and create a new folder inside the app
directory and name it details
, then create a file [id].tsx
inside this folder. This will serve as our dynamic route screen, where characters' details will be shown. Expo router makes it easy to add navigation on the fly. Add the code to our just created screen.
import { View, Text, ViewStyle, TextStyle, Image } from "react-native";
import React, { useEffect, useState } from "react";
import { Link, Stack } from "expo-router";
import { useSearchParams } from "expo-router";
import { getCharacter } from "../../api";
const Details = () => {
const [character, setCharacter] = useState<any>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const { id } = useSearchParams();
useEffect(() => {
const fetchCharactersData = async () => {
try {
const res = await getCharacter(id);
setCharacter(res);
} catch (err) {
setLoading(false);
setCharacter([]);
setError("An error occurred");
} finally {
setLoading(false);
}
};
fetchCharactersData();
}, []);
return (
<View style={$container}>
{error && <Text style={$errorText}>{error}</Text>}
{loading && (
<Image
source={{
uri: "https://res.cloudinary.com/evergreenx/image/upload/v1680297056/Loading_component_tr2ec6.png",
}}
resizeMode="contain"
style={{
width: 300,
height: 200,
alignSelf: "center",
marginTop: 100,
}}
/>
)}
{character && (
<>
<Link
href={{
pathname: "/",
}}
style={$backButton}
>
<Text style={$backButtonText}>Back</Text>
</Link>
<View style={$characterImageContainer}>
<Image
source={{ uri: character?.image }}
style={{
width: 250,
height: 250,
borderRadius: 150,
}}
resizeMode="contain"
/>
<Text style={$characterName}>{character?.name}</Text>
</View>
<View style={$characterInfoContainer}>
<View style={$characterInfo}>
<Text style={$characterText}>Gender: </Text>
<Text style={$characterTextInfo}>{character?.gender}</Text>
</View>
<View style={$characterInfo}>
<Text style={$characterText}>Status: </Text>
<Text style={$characterTextInfo}>{character?.status}</Text>
</View>
<View style={$characterInfo}>
<Text style={$characterText}>Species: </Text>
<Text style={$characterTextInfo}>{character?.species}</Text>
</View>
<View style={$characterInfo}>
<Text style={$characterText}>origin: </Text>
<Text style={$characterTextInfo}>{character?.origin.name}</Text>
</View>
<View style={$characterInfo}>
<Text style={$characterText}>location: </Text>
<Text style={$characterTextInfo}>{character?.location.name}</Text>
</View>
</View>
</>
)}
</View>
);
};
export default Details;
const $container: ViewStyle = {
flex: 1,
width: "100%",
backgroundColor: "#FFDEAD",
paddingVertical: 40,
paddingHorizontal: 20,
};
const $characterImageContainer: ViewStyle = {
width: "100%",
padding: 15,
borderRadius: 10,
display: "flex",
justifyContent: "center",
alignItems: "center",
};
const $characterName: TextStyle = {
color: "#081F32",
marginVertical: 16,
fontSize: 30,
fontWeight: "400",
};
const $characterInfoContainer: ViewStyle = {
width: "100%",
padding: 15,
borderRadius: 10,
};
const $characterText: TextStyle = {
color: "#081F32",
marginVertical: 3,
fontSize: 16,
fontWeight: "700",
textTransform: "capitalize",
display: "flex",
flexDirection: "row",
};
const $characterTextInfo: TextStyle = {
color: "#6E798C",
fontSize: 14,
fontWeight: "400",
};
const $characterInfo: ViewStyle = {
padding: 5,
marginVertical: 3,
borderBottomColor: "#979797",
borderBottomWidth: 0.5,
};
const $backButton: ViewStyle = {
width: 100,
height: 40,
borderRadius: 10,
display: "flex",
justifyContent: "center",
alignItems: "center",
marginBottom: 20,
marginTop: 10,
};
const $backButtonText: TextStyle = {
color: "#979797",
fontSize: 16,
fontWeight: "700",
};
const $errorText: TextStyle = {
color: "red",
fontSize: 16,
fontWeight: "700",
alignSelf: "center",
paddingTop: 100,
};
In the code above we import Link and useSearchParams for expo router. We will be using Link to a back button and useSearchParams to return the URL search parameters. we imported the async function getCharacter
which is yet to be created.
A useEffect to fetch our data for a single character by the id
. Check API documentation here. We are setting some states for our data. lastly, we are returning JSX based on our data. for the Link
component from expo router, it takes a href
prop. and this takes an object with a pathname that /
represents our home screen. we could also define this way <Link href="/" >
Now let's update our api
file with the getCharacter
function. replace your code with this below.
import { create } from "apisauce";
import { ApiResponse, Character } from "./interface";
const api = create({
baseURL: "https://rickandmortyapi.com/api/",
headers: { Accept: "application/json" },
});
export const getCharacters = async (): Promise<ApiResponse> => {
const response = await api.get("/character");
if (!response.ok) throw new Error(response.problem);
return response.data as ApiResponse;
};
export const getCharacter = async (id: string): Promise<Character> => {
const response = await api.get(`/character/${id}`);
if (!response.ok) throw new Error(response.problem);
return response.data as Character;
};
We added a new async function getCharacter
to fetch a single character based on the id
.
Let update CharacterCard.tsx
so when we click on a card we will be taken to a new screen with details containing that particular character.
Update the component with the code below.
import {
View,
Text,
FlatList,
TextStyle,
ViewStyle,
Image,
Pressable,
} from "react-native";
import React from "react";
import { Character } from "../interface";
import { useRouter } from "expo-router";
type CharacterCardProps = {
characters: Character[];
};
export default function characterCard({ characters }: CharacterCardProps) {
const router = useRouter();
const renderCharacters = ({ item }: { item: Character }) => {
return (
<Pressable
style={$characterContainer}
onPress={() => {
router.push({
pathname: "details/id/",
params: { id: item?.id },
});
}}
>
<View style={$characterImageContainer}>
<Image
source={{ uri: item?.image }}
style={{
width: "100%",
height: 150,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
borderRadius: 10,
}}
/>
</View>
<View style={$characterInfoContainer}>
<Text style={$characterText}>{item?.name}</Text>
<Text style={$characterSpecies}>{item?.species}</Text>
</View>
</Pressable>
);
};
const renderHeader = () => {
return (
<View>
<Image
source={{
uri: "https://res.cloudinary.com/evergreenx/image/upload/v1680259111/Logo_1_tdttqu.png",
}}
style={{ width: 300, height: 200, alignSelf: "center" }}
resizeMode="contain"
alt="logo"
/>
</View>
);
};
return (
<View style={$container}>
<FlatList
style={{ width: "100%", padding: 10, marginBottom: 20 }}
data={characters}
renderItem={renderCharacters}
keyExtractor={(item) => item.id.toString()}
showsVerticalScrollIndicator={false}
numColumns={2}
ListHeaderComponent={renderHeader}
/>
</View>
);
}
const $container: ViewStyle = {
flex: 1,
alignItems: "center",
justifyContent: "center",
};
const $characterContainer: ViewStyle = {
backgroundColor: "#fff",
height: 230,
width: "45%",
borderRadius: 10,
marginVertical: 10,
marginHorizontal: 10,
};
const $characterImageContainer: ViewStyle = {
width: "100%",
overflow: "hidden",
marginBottom: 10,
};
const $characterText: TextStyle = {
color: "#000",
fontSize: 18,
fontWeight: "500",
};
const $characterSpecies: TextStyle = {
color: "#000",
fontSize: 13,
fontWeight: "500",
marginTop: 5,
};
const $characterInfoContainer: ViewStyle = {
padding: 10,
};
The new changes contained in the code above, we imported useRouter
hook from expo router, and we will use it to navigate to the dynamic screen we created earlier. as stated in the docs we could use the Link component to move between and we can also use the useRouter hook to navigate imperatively.
On our card, we added a Pressable
component, onPress of the card we will be taken to the details/id
and we are passing a param to the screen which is id
representing a single character. Then use the id
to get data from our API for that particular character.
When we click on a card, we should get a screen like this containing details of the character you clicked on.
Conclusion
In the article, we have explored how to use the Expo Router to add navigation to our app and also how we could fetch data from an API using apisauce.
This is a little of what we could do with Expo Router, check out the docs to learn more.
Top comments (0)