One of the hardest, unsolved problems in (cross-platform) app development is navigation. There's seemingly no right way to set up navigation in an app but there are a lot of wrong ways. Building native navigation is a manual, platform-specific, painstaking process that almost always falls short of its full potential (robust universal links, app indexing, handoff, app clips, etc.).
On the opposite end of the spectrum, you have web routing which is fast to build, easy(ish) to understand, discoverable, and extremely scalable. Outside of gesture-driven animations, web navigation is the gold standard. So why not bring it to mobile development?
Getting a web-like system working for native apps is very difficult. Native has a lot of internal state that cannot be easily represented by a URL without assuming a lot of reasonable defaults at every step. Native apps need to run offline without a server. Native apps are also traditionally built with compiled languages making it hard to dynamically parse the file structure of the project in a way that's fast and responsive to the developer.
The opinionated solution
Since April 2021, I've been working on a new cross-platform router that generates nested navigation and deep links based entirely on the project's file structure. The concept of a file system-based router is not new to web developers (PHP, Next.js, etc.), but it is brand new to mobile development. By bringing this foundational paradigm to mobile, we move much closer to matching the discoverability, and scalability of websites in native apps.
There are many applications for the new routing system. The most important goal is to make it fast and easy to create, maintain, and scale apps with nested native navigation. More features can be layered on top in later versions.
Discoverable and scalable
Expo Router is a nested system that generates links for each leaf route in the app. This lets users open and share any part of the app instantly with anyone. This feature is critical for content-driven applications like social media, news, and e-commerce, really anything content-driven.
Imagine you wanted to share a photo with a friend so you sent them a link to instagram.com and told them to search around until they find the exact page you were thinking about. This is effectively how an app without deep links operates, and that leads to poor user adoption.
The web solved this problem decades ago by associating every page with a URL that can be instantly shared and visited by anyone. We plan to bring this same flexibility to mobile apps via Expo Router.
Because Expo is multi-platform, you can develop your routes once and share them across iOS, Android, and the web. This means you can utilize the built-in URL handling of the browser to quickly build out your native app's routes.
Ultimately we'd like to make all native apps support app search and content indexing automatically. This will enable native search engines to index content in your app, making content discoverability radically better.
Offline-First and Fast
Unlike web apps, native apps are all expected to work offline. This means that the app must be able to handle any incoming URL without a network connection or server.
Navigation is truly native and optimized for iOS and Android
The end-user experience is fast and light-weight since the router can change the loading policy of components dynamically between development and production — currently this only saves on JS execution time, we plan to add the option for bundle splitting leaf nodes in a future version. The developer experience is equally fast utilizing universal Fast Refresh, incremental bundling, and multi-layered artifact memoization in the bundler.
Data Fetching and Error Handling
One of the most exciting parts of React is the ability to divide your app's UI and data fetching into reusable components.
React Error Boundaries can be nested in layouts enabling users to navigate away or retry the page.
Expo Router has the ability to set up React Error Boundaries for each route (Suspense boundaries coming in a later version). This means that you can easily handle errors and data fetching states in a consistent way across the entire app and retry failed requests.
This is a large departure from the traditional approach to handling errors in a native app: throwing a fatal error and crashing the app with no indication of what went wrong. This is a terrible user experience and something we're excited to move away from.
The initial version of the Expo Router just scratches the surface of data fetching and error handling, more on this in a future release. We've been considering it since the beginning because it is integral to the framework's design and we want to ensure we are not locking developers into a system that cannot handle these features in the future.
The following is a simple example of migrating a single-screen app from the most popular navigation library for React Native, React Navigation.
Expo Router builds on top of React Navigation making it easier to migrate existing React Native apps to file system-based routing.
No boilerplate needed! Apps now have multiple static entry points which makes it possible for tooling to reason about the structure of your app and make optimizations behind the scenes.
❌ Before Expo Router
For the sake of brevity, I've omitted the dependency installation steps and native Xcode / Android Studio steps required when working without Expo. This example also doesn't account for hiding the splash screen after navigation has mounted.
// Must be updated to match the screens in the project
Finally create an entry file for registering the root componet:
✅ After Expo Router
Delete the index.js and App.js — Simply create a file in the app directory, and export a React component. The framework will generate the navigation and deep links in-memory, instantly with Fast Refresh!
You can start the app with npx expo start and visit the /home route on any device. I recommend using uri-scheme to test deep links on iOS and Android. You can also navigate to /_sitemap to reach the sitemap (development only):
Apache index throwback.
As you can imagine, this scales nicely and is very easy to maintain. You can create as many pages as you want, and the framework will accommodate the linking structure in a reliable way. See the official setup guide here.
There is some temporary additional setup during the beta which requires an extra babel plugin to be used. This will go away in the official release.
If you navigate to a route that isn't matched, the default behavior is to have a 404-esque response which enables the user to navigate back to the root path / — this can be replaced by using a top-level deep dynamic route.
Virtual pages have a recurring theme.
Request for Comments
We're excited to share the early beta of the Expo Router with you today. We want to hear your feedback and help us push the boundaries of what's possible!
We're looking for feedback on the following:
API Design: Specifically the routing convention and layouts API. We want to make sure the API is intuitive and easy to use.
Tooling and workflow: We want to make sure the tooling is fast and easy to use. We also want to make sure the workflow is intuitive and easy to understand, this is especially true for navigating URLs on a native device where there is no URL bar.
Performance: We want to make sure the app is fast and reliable. We also want to make sure the app is small and lightweight. We're currently focused on “make it work”, with “make it fast” features like bundle splitting, and suspense boundaries coming in a future version. The file system-based router enables our tooling to implement many performance optimizations behind the scenes without needing to refactor in the future.
This beta wouldn't have been possible without the help of some talented developers:
Parts of the public-facing API are a native-ified version of the popular web frameworks Remix (nested routes) and Next.js (file conventions). With additional API inspiration from SvelteKit (groups → fragments) and Redwood (error boundaries are similar to cells). Whatever's leftover was inspired by PHP. I tried to keep the API feeling as familiar as possible to devs who have previous experience using React frameworks.