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 vs. React Native
- What is Expo Router?
- React Navigation vs. Expo Navigation
- Building a React Native contacts app with Expo Router
- Getting started
- Creating the contact model with AWS Amplify
- Initializing the backend environment
- Configuring the app with DataStore
- Rendering contacts with a static route
- Rendering single contacts with a dynamic route
- Navigating between tabs
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:
You should see something like the following:
Click the Data menu by the left sidebar of the dashboard, then click Save and Deploy:
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:
When it’s done deploying, open the Content menu:
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:
Our final contacts app, built with the new Expo Router
Navigating between tabs
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> <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:
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.