DEV Community

ido dickson evergreen
ido dickson evergreen

Posted on • Originally published at blog.idoevergreen.xyz on

Routing in Expo: Expo Router

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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";

Enter fullscreen mode Exit fullscreen mode

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"
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

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"
  }

Enter fullscreen mode Exit fullscreen mode

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")],
  };
};

Enter fullscreen mode Exit fullscreen mode

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",
};

Enter fullscreen mode Exit fullscreen mode

Now restart your server.

npx expo start

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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[];
  }

Enter fullscreen mode Exit fullscreen mode

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;
};

Enter fullscreen mode Exit fullscreen mode

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",
};

Enter fullscreen mode Exit fullscreen mode

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,
};

Enter fullscreen mode Exit fullscreen mode

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,
};

Enter fullscreen mode Exit fullscreen mode

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;
};

Enter fullscreen mode Exit fullscreen mode

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,
};

Enter fullscreen mode Exit fullscreen mode

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.

Credits

Top comments (0)