![]() |
VOOZH | about |
Editorβs note: This article was last updated 16 June 2023 to include an example of navigating between tabs using Expo Router.
π A guide to native routing in Expo for React NativeA 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:
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
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 and React Native are often interchanged for one another. Although they have similar features, there are a few important differences between them:
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.
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.
Letβs review the core features of Expo Router:
app directoryLink componentimport { 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 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.
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:
npm install -g @aws-amplify/cliamplify configurenpm install -g expo-cliFeel free to check out the GitHub repo with the complete code for the demo.
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.
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.
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.
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)
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.
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].
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>
<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
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's Galileo AI watches sessions for you and and surfaces the technical and usability issues holding back 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.
Learn how next-browser gives AI agents runtime context for debugging Next.js apps, including React props, hydration, PPR, forms, and performance.
Build dynamic LLM routing in Next.js with OpenRouter, TanStack AI, task classification, model fallbacks, and cost-aware routing.
TSRX adds first-class control flow, conditional hooks, and scoped styles to React via a TypeScript compiler extension β no new framework required.
Learn how to build a full React Native auth system using Better Auth and Expo β with email/password login, Google OAuth, session persistence, and protected routes.
Would you be interested in joining LogRocket's developer community?
Join LogRocketβs Content Advisory Board. Youβll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now