Emmanuel John I'm a full-stack software developer, mentor, and writer. I am an open source enthusiast. In my spare time, I enjoy watching sci-fi movies and cheering for Arsenal FC.

A guide to native routing in Expo for React Native

8 min read 2501

A guide to native routing in Expo for React Native

Editor’s note: This article was last updated 16 June 2023 to include an example of navigating between tabs using Expo Router.

A decade ago, if anyone told you that you could build native mobile applications using JavaScript without compromising UX, you wouldn’t believe it, right? Then, React Native came along and made it possible. Similarly, a few years ago, if someone told you that you could build cross-platform apps using JavaScript without heavy emulator or developer environments, like Android Studio or Xcode, you wouldn’t believe it either, right? Then, Expo was created.

In this article, we’ll review some pros and cons of Expo Router, which is in v1 at the time of writing. To demonstrate its core concepts, we’ll build a React Native application using Expo Router for navigation. Let’s get started!

Jump ahead:

What is Expo?

Expo is an open source platform and set of tools that simplifies the process of building, deploying, and maintaining React Native applications. It provides a development environment, build infrastructure, and a collection of libraries and APIs that enhance the development experience.

With Expo, developers can create cross-platform mobile applications using JavaScript and React Native without needing extensive native code development or configuration. It abstracts away the complexities of setting up and managing native projects, allowing developers to focus on writing code and building features.

Expo vs. React Native

Expo and React Native are often interchanged for one another. Although they have similar features, there are a few important differences between them:

  • Expo builds a layer of tools on top of React Native, allowing developers to build applications without writing any native code
  • Expo CLI allows developers to create, deploy, and open apps on their devices
  • Expo’s client app, Expo Go, allows you to open your projects without needing Android Studio or Xcode

However, Expo and React Native share one major feature, navigation. In React Native applications, routing is implemented using React Navigation, which wraps your app with a navigator component that manages the navigation history and presentation of screens in the app.

Although this worked well, it had some issues. For example, you had to install peer dependencies like react-native-screens and react-native-safe-area-context, which add extra weight to your app.

What is Expo Router?

We all know that JavaScript developers love file-based routing. Next.js’s file-based routing style works well and has quickly become the current standard for routing in JavaScript applications. This led Evan Bacon, the creator of Expo, to build a new library called Expo Router, which works similarly to the Next.js router. Both generate nested navigation and deep links based entirely on a project’s file structure.

Expo Router is a routing concept that extends the functionality of the React Navigation suite. If you’re already familiar with how navigation works in Next.js, understanding Expo Router should be pretty straightforward. Essentially, every file in your app directory becomes a route in your application, making it easy to manage navigation within your app.

Core features of Expo Router

Let’s review the core features of Expo Router:

  • Offline-first and fast: Native apps must handle incoming URLs without an internet connection. Expo Router enables this by implementing these features across the entire framework
  • Error handling: You can set up React error boundaries on each route
  • Layout routes: Most screens in your app will share the same layout components. Expo Router allows you to create a parent layout component in the app directory
  • Next.js-like linking and dynamic and static routing: Similar to the Next.js router, you can link routes using the Link component
import { Link } from "expo-router";

export default function Page() {
return (

Home
);
}

So far, the only major tradeoff with the new Expo Router is that it is limited and opinionated. Some developers may like the Expo Router features but want to rearrange the file directory structure. Unfortunately, due to its limitations, you might not be able to customize your Expo Router instance to fit your preferred structure just yet.

React Navigation vs. Expo Navigation

React Native Navigation and Expo Navigation are two popular navigation solutions for React Native applications. While they serve a similar purpose of handling navigation within an app, there are some noteworthy differences between them.

React Native Navigation provides a rich set of features, including support for bottom tabs, side menu drawers, stack-based navigation, and deep linking. It also offers advanced customization options, allowing you to finely control the navigation stack and perform custom transitions. However, it requires some native setup and configuration, making the initial setup a bit more complex compared to other solutions.

Expo Navigation offers a declarative API and a set of configurable navigators, including Stack Navigator, Tab Navigator, Drawer Navigator, and Switch Navigator. It provides a simple and intuitive way to define navigation flows and screens using components and props. Designed to work out of the box with Expo-managed projects, Expo Navigation eliminates the need for complex native configurations.

Expo Router, in general, makes app development faster and more intuitive for developers. Deep linking works right out of the box, and Expo Router is naturally URL-based, making it simple to target webpages.

React Navigation is unquestionably primitive, and you must create your own patterns for deeplinks, stack nesting, etc. Expo Router provides thoughtful APIs, but they may be too limiting. Expo Router is easier to use due to its simpler DX.

Building a React Native contacts app with Expo Router

We’ll build a contacts directory app that uses the Expo Router to navigate between screens. To follow along with this tutorial, you’ll need the following:

  • Node.js ≥v14 installed
  • Knowledge of JavaScript and React
  • AWS Amplify CLI installed: npm install -g @aws-amplify/cli
  • AWS Amplify configured: amplify configure
  • Expo CLI installed: npm install -g expo-cli
  • Expo Go: Installed from your mobile play store

Feel free to check out the GitHub repo with the complete code for the demo.

Getting started

Let’s get started by scaffolding a new Expo app. Run the command below in your terminal:

npx create-expo-app@latest --example with-router

The code above will create a new React Native application with Expo Router configured. Change the directory, initialize Amplify, and install peer dependencies using the following commands in your terminal:

cd ReactNativeContactExpoApp
npx amplify-app@latest
npm install aws-amplify @react-native-community/netinfo @react-native-async-storage/async-storage

To run the app, run yarn start or npm run start:

Starting Metro Bundler
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
█ ▄▄▄▄▄ █▄▄███▀ ███ ▄▄▄▄▄ █
█ █   █ █ ▀█ ▄ █▄██ █   █ █
█ █▄▄▄█ █▄ ▄▄▀▀█▄██ █▄▄▄█ █
█▄▄▄▄▄▄▄█▄▀▄▀▄█ █ █▄▄▄▄▄▄▄█
█▄▄▄ ▀▀▄█ ▀████▀██▄██▄ ▄▀▄█
█▀▄▀▄▄▄▄▀▄██▀▀▀▄█▄█ ▀██▀███
█▄▀█▀ ▄▄▀▄▄▀ ▄ █ ▄█ ▄ █ █▀█
█▀▄▀██▀▄█ ▀ █▀███ ▀▀█▀▀█ ▀█
███▄▄▄▄▄█ ▄▀ ▄  ▄ ▄▄▄ ▀▄█▀█
█ ▄▄▄▄▄ ███ ▀▄███ █▄█ █▄  █
█ █   █ █▀▀█▀ ▀▀▀▄▄   █▀▀ █
█ █▄▄▄█ █  ▄▄█▀ ▀▄▀▄█▄▄ ▄██
█▄▄▄▄▄▄▄█▄█▄██▄█▄██████▄▄▄█

› Metro waiting on exp://192.168.55.200:19000
› Scan the QR code above with Expo Go (Android) or the Camera app (iOS)

› Web is waiting on http://localhost:19000

› Press a │ open Android
› Press i │ open iOS simulator
› Press w │ open web

› Press j │ open debugger
› Press r │ reload app
› Press m │ toggle menu

› Press ? │ show all commands

Scan the QR code on your mobile Expo Go app, and it’ll compile.

Creating the contact model with AWS Amplify

Just like with every other database, DataStore requires a model. To generate this model, head to the Amplify Directory > Backend > API > (Amplify project name) > schema.graphql. Modify schema.graphql with the following code:

type Contact @model {
  id: ID!
  name: String!
  phone: String!
  email: String!
  address: String
  message: String
}

The contact model has id, name, title, phone, and email fields. Let’s go ahead and generate the model with the following command in your terminal:

npm run amplify-modelgen

The code above will create a src/models folder with the model and GraphQL schema.



Initializing the backend environment

We need to initialize an Amplify backend environment for the application. To do this, run the following command in your terminal:

amplify init
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
Using default provider  awscloudformation
? Select the authentication method you want to use: AWS profile

This will create a configuration file in src/aws-config.js. Next, we’ll deploy the backend and create AWS resources using the following command:

amplify push
...
Do you want to generate code for your newly created GraphQL API? No
...

Depending on the quality of your internet connection, this might take some time to deploy.

Configuring the app with DataStore

Let’s go ahead and configure the Expo app to work with Amplify. Create an app/index.js file and add the code below:

import config from '../src/aws-exports';
import { DataStore, Amplify } from 'aws-amplify';

Amplify.configure(config)

Creating contacts using Amplify Studio

Now, let’s add some contacts to our DataStore. Head to the AWS Amplify console in your browser. As a shortcut, you can run amplify console in your terminal and select AWS Console. To create contacts with Amplify Studio, go to the Amplify dashboard and click Launch Studio:

Amplify Dashboard Launch Studio

You should see something like the following:

Save Deploy React Native Expo Amplify

Click the Data menu by the left sidebar of the dashboard, then click Save and Deploy:

Amplify Studio Data Modeling Page

You can see the schema that we created and edit it using the UI. Deploying the data model might take few minutes, depending on your internet connection:

Successful Deploy React Native Model

When it’s done deploying, open the Content menu:

Open Content Menu Amplify Studio

To create a new contact, click the Create contact button. You can also auto-generate seed data by clicking the Actions dropdown and selecting Auto-generate data. That’s it! We’ve successfully created contacts for the application.

Rendering contacts with a static route

Now, let’s render the contacts. Update the app/index.js file with the following code:

import { View, Text, Button } from "react-native";
import { Link, Stack } from "expo-router";
import config from '../src/aws-exports';
import { DataStore, Amplify } from 'aws-amplify';
import { Contact } from '../src/models'
import { useState } from "react";

Amplify.configure(config)

export default function Home() {
    const [contacts, setContacts] = useState([])

    async function fetchContacts() {
        const allContacts = await DataStore.query(Contact)
        setContacts(allContacts)
    }

    fetchContacts()

    return (
        <View style={container}>
            <Stack.Screen options={{ title: "Contacts" }} />
            {
                contacts.map(contact => (
                    <View key={contact.id} style={contactBox}>

                        <View>
                            <Text style={textStyleName}>{contact.name}</Text>
                            <Text style={textStyle}>{contact.phone}</Text>

                        </View>
                        <Link href={`contacts/${contact.id}`}>
                            <Button
                                title="View contact"
                                color="#841584"
                            />
                        </Link>
                    </View>
                ))
            }
        </View>
    );
}

In the code above, we query the contacts from the DataStore by passing the model to the DataStore .query() method:

DataStore.query(Contact)

To get more details for each contact, we used the new Expo Router link component and passed the \[contact.id\](<http://contact.id>) to the href attribute. The dynamic route will be contacts/[contactId].

Rendering single contacts with a dynamic route

Let’s create the screen for contact details. In the app directory, create a contacts folder and an [id].js file:

import { Text, View } from "react-native";
import { Stack } from "expo-router";
import { useEffect, useState } from "react";
import { Contact } from "../../src/models";
import { DataStore } from "aws-amplify";

export default function SingleContact({ route }) {
    const [contact, setContact] = useState('')
    useEffect(() => {
        if (!route.params?.id) {
            return
        }
        DataStore.query(Contact, route.params.id).then(setContact)
    }, [route.params?.id])
    return (
        <View style={container}>
            <Stack.Screen options={{ title: contact.name }} />
            <View style={contactBox}>
                <Text style={textStyleName}>{contact.name}</Text>
                <Text style={textStyle}>{contact.phone}</Text>
                <Text style={textStyle}>{contact.email}</Text>
                <Text style={textStyle}>{contact.address}</Text>
                <Text style={textStyle}>{contact.message}</Text>
            </View>
        </View>
    );
}

If you’ve worked with Next.js, this code should look familiar. The major difference is that the Expo Router makes the route object available without importing it.

In DataStore, to query a single item by its ID, you pass the model and ID you want to query. In our case, we pass the params ID. Run your app, and you should get something like the following:

Final Contacts App React Native Expo Amplify Demo

Our final contacts app, built with the new Expo Router

To achieve navigation between tabs using Expo Router, first, run the following code to create a project with expo-router set up:

npx create-expo-app@latest --example with-router

The command above initializes and creates our application with routing functionality. Next, run the following command to start your Expo app:

npm start

Click on the touch app/index.js button on the bottom screen of your emulator to create the app directory with the index.js file. Update the index.js file as follows:

import { Redirect } from "expo-router";
export default function Page() {
  return <Redirect href={'/home'}/>
}

In the app directory, create _layout.js and add the following code:

import { Stack } from "expo-router";
export default () => {
    return <Stack
        screenOptions={{
            headerStyle: {
                backgroundColor: "green"
            },
            headerTintColor: "#1E2632",
            headerTitleStyle: {
                fontWeight: "bold"
            }
        }}
    />
};

In the example above, we use Stack from Expo Router to create a stack navigator. We set the screenOptions prop to customize the header styles for all screens in the stack. In the app directory, create home/feed.js and add the following code:

import { View, Text } from "react-native";
const Home = () => {
  return (
    <View>
      <Text>Home</Text>
    </View>
  );
};
export default Home;

In the home directory, create inbox.js and add the following:

import { View, Text } from "react-native";
const Inbox = () => {
  return (
    <View>
      &lt;Text>Inbox</Text>
    </View>
  );
};
export default Inbox;

In the app directory, create home/_layout.js and add the following:

import { Tabs } from "expo-router";
import { FontAwesome } from "@expo/vector-icons";
export default () => {
  return (
    <Tabs screenOptions={{ tabBarShowLabel: false }}>
      <Tabs.Screen
        name="feed"
        options={{
          tabBarIcon: ({ color }) => (
            <FontAwesome name="home" size={24} color={color} />
          ),
          title: "Feed",
        }}
      />
      <Tabs.Screen
        name="inbox"
        options={{
          tabBarIcon: ({ color }) => (
            <FontAwesome name="envelope" size={24} color={color} />
          ),
          title: "Inbox",
        }}
      />
    </Tabs>
  );
  // return <Tabs/>
};

In the code above, we use Tabs from Expo Router to create a bottom tab navigator. We set the screenOptions prop to customize the tab icons based on the route name using the FontAwesome icon library. Additionally, we use color props to specify the colors for active and inactive tabs. Now, you should see the following result:

Navigating Between Tabs Expo Router

Conclusion

In this article, we learned about Expo Router, how it works, its core features, and its tradeoffs. We built a React Native app with Expo and data from Amplify DataStore. We also built a tab navigator to achieve navigation between tabs using Expo Router.

File-based routing is the future of smooth navigation experience for mobile applications, and Expo Router implements this solution into its library. I hope you enjoyed this article, and be sure to leave a comment if you have any questions. Happy coding!

LogRocket: Instantly recreate issues in your React Native apps.

LogRocket is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.

LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.

Start proactively monitoring your React Native apps — try LogRocket for free.

Emmanuel John I'm a full-stack software developer, mentor, and writer. I am an open source enthusiast. In my spare time, I enjoy watching sci-fi movies and cheering for Arsenal FC.

Leave a Reply