Introducing Expo Router v5: the most powerful way to build full-stack native apps

ProductDevelopment12 minutes read

Evan Bacon

Evan Bacon

Engineering

Expo Router v5 is here—now stable, faster, and full-stack ready. Build universal apps with React Server Functions, API Routes, and better error logging.

Introducing Expo Router v5: the most powerful way to build full-stack native apps

At Expo we believe in building truly great software that runs where it matters most. Our state-of-the-art framework — Expo Router v5 — is now officially stable and has some incredible new features.

This release is focused primarily on stability, and reliability on native. More full-stack functionality, and performance improvements everywhere!

Improved Errors and Logging

We completely redesigned React Native’s default error logging in Expo CLI.

  • Stack traces are now human-readable. No more long URLs.
  • Most useful frame prints a code preview.
  • The [NOBRIDGE] tag has been removed.

API Routes in Production

In Expo Router v3 we introduced experimental support for API Routes in Expo Router, enabling devs to build and ship full-stack universal apps with the best modern practices and APIs.

While these worked great in development, production releases got even more complicated. Because shipping is so fundamental to Expo, we’ve added first-class support for deploying servers with your native build.

We built EAS Hosting with universal app development in mind. You can enable server output in your app.json, and toggle the experimental EXPO_UNSTABLE_DEPLOY_SERVER environment variable to automatically build, deploy, and link versioned deployments to your app. This will set the window.location.origin to the server origin in production and ensure fetch requests are made relative to this origin, e.g.

Code
const data = await fetch("/api/endpoint");
// Fetches from: https://myapp-xxx.expo.app/api/endpoint
// on all platforms.

You can also lock the origin to a given URL by defining it in your app.json to enable unversioned access to the latest endpoints:

app.json
{
"plugins": [
["expo-router", { "origin": "https://myapp.expo.app" }]
]
}

We’re continuing to build out this functionality to create the best universal app development platform ever made. Share your experience and feedback with us to help improve the system.

Learn more in Expo API Routes.

React Server Functions (Beta)

First announced at React Conf 2024, React Server features are coming to Expo Router. React Server Functions and Components will enable best-in-class server-driven UI for everyone, across all platforms!

Spotify Demo

With React Server Functions, you can easily create typed server endpoints that have access to secret environment variables from your .env file. This makes authentication substantially easier.

Code
"use server";
// This code runs securely on the server.
const AUTH = Buffer.from(`${process.env.CLIENT_ID}:${process.env.SECRET}`).toString("base64");
export async function exchangeAuthCode<T>(code: string, redirectUri: string): Promise<T> {
const body = await fetch("https://accounts.spotify.com/api/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${AUTH}`,
},
body: new URLSearchParams({
code,
redirect_uri: redirectUri,
grant_type: "authorization_code",
}),
}).then((res) => res.json());
return body;
}

React Server Functions are like API Routes but with first-class React and TypeScript support. They respect platform extensions like .ios.ts when invoked from an iOS client, the have support for process.env.EXPO_OS to determine which client they’re rendering for, and they can return JSX that is streamed to the client.

You can render components on the server and stream them down to the client:

functions/profile.tsx
"use server"
import { Image } from 'react-native';
export async function renderProfile(username: string) {
// Securely perform data fetching and access secrets...
const profile = await fetch('...').then(res => res.json());
// Return some React JSX——these are React Server Components.
return <Image source={{ uri: profile.picture }} />
}

Which can be used on the client by invoking and wrapping with suspense:

app/profile.tsx
import { renderProfile } from '@/functions/profile'
export default function Profile() {
const username = "evanbacon"
return (
<React.Suspense fallback={<Text>Loading...</Text>}>
{renderProfile(username)}
</React.Suspense>
)
}

React Server features can be deployed to EAS Hosting the same as API Routes.

We love using React Server Functions in Expo Router and have built a number of open-source demos with it already:

Learn more in React Server Components in Expo. Enable the feature in the app.json:

React 19+

Expo Router v5 uses React 19 which means you can migrate forwardRef to standard components and leverage the new state management improvements like use:

Code
function Header({ children }) {
if (children == null) {
return null;
}
// Conditionally select the context from the provider
const theme = use(Theme);
return (
<Text style={{ color: theme.color }}>
{children}
</Text>
);
}

With Expo SDK 53, the new React Canary version is 19.1, which you can use to enable the new owner stack system. Owner stack in Expo will show you even more frames that are closer to the original React error, making it much easier to locate well-known issues such as unwrapped text, or undefined components.

Enable the experimental react canary in the app.json like so:

app.json
{
"experiments": {
"reactCanary": true
}
}

We plan to make React 19.1 the default version in Expo Router v6 / SDK 54 later in 2025. React 19.1 is used by default with React Server features.

Linting

As part of our improved error handling, we’ve also improved the default rules in our ESLint config and enabled it by default in the starter template.

React issues like “Missing "key" prop for element in iterator” will now be shown inline during development, making it easier to spot and fix before issues occur at runtime.

We’ve also restricted use of certain CJS features such as require when not importing an asset to move users toward modern ESM language features that can be better optimized by Expo Tree Shaking.

You can also now use npx expo lint as a drop-in replacement for npx eslint but with expo-first defaults to ensure speed and efficiency when using it.

Learn more in our new best practices blog post.

React Compiler

Using npx expo lint will prepare your project for React Compiler which can be used to dramatically improve performance in your app by automatically memoizing components. This is huge for Expo Router as screen transitions will be noticeably more performant.

React Compiler before/after

In-app Redirection

Authentication and authorization are both fundamental features in many apps. With Expo Router v5 we’ve added first-class primitives for working with both.

Traditional web authorization works by validating the request on the server before returning access to content or screens from the server.

However, native apps traditionally work by shipping all layout and UI logic locally, meaning any user has access to the source code for all screens at all times.

To account for this, we’ve introduced a number of primitives: API Routes, secure environment variables, server-driven UI with React Server Components, and client-side routing helpers.

If you have components that you wish to protect, you can conditionally render these on the server and stream them down when an authorized request is made using React Server Functions in Expo Router.

When you have a route that defers all authorized logic but you don’t want users to navigate to it at runtime, you can use the new <Stack.Protected /> semantic to automatically perform client-side redirects of entire groups of requests.

Code
import { use } from 'react';
import Stack from 'expo-router/stack';
function Layout() {
const user = use(AuthStore)
return (
<Stack>
<Stack.Screen name="login" />
{/* If the user is not logged in, Expo Router will automatically redirect away. */}
<Stack.Protected guard={!user}>
<Stack.Screen name="profile" />
<Stack.Screen name="settings" />
</Stack.Protected>
</Stack>
)
}

This feature required some re-architecture to create an incremental local representation of the navigation tree that is reconciled after all components have mounted.

Resetting Navigation State

When a user signs out of your app, you’ll likely want to erase as much in-memory state as possible.

This is traditionally pretty challenging in a native app because you need to keep track of all possible state and globals. In Expo Router you can simply call location.reload() to reload the Hermes engine and clear all runtime React/JavaScript state. This is akin to how the web works when you server navigate to the unauthenticated page. Performing a full reload ensures all in-memory JavaScript is cleared and no state can be leaked to unauthorized users.

Code
function signOut() {
// Update React context and cached state.
setAuthState(null);
// Reload the app.
location.reload();
}


By leveraging the standard web API, we reduce the risk of breaking changes between releases.

Server Redirects

You can now configure server redirects and rewrites statically in the app.json. This is great for migrations and supporting brownfield projects.

You can define matchers using the Expo Router file convention and server redirect to different outputs.

app.json
{
"plugins": [
["expo-router", {
"redirects": [{
"source": "/u/[...users]",
"destination": "/users/[...users]"
}]}
]
]
}

This config is only used on the server. Client-side navigation on both native and web will require client-side redirect logic in addition to these redirects. Native apps will only use the server redirects when making fetch requests to the server.

EAS Hosting will pick up the redirects logic and apply it automatically. Learn more in static redirects.

Anchor Route

initialRouteName is now called anchor

An anchor route is a route that is always rendered in a stack. When you deep link to a modal, it could render differently to when you client-navigate to the same route. In these cases, you need to design navigation more conditionally to allow for different initial experiences. An anchor route enables you to indicate that all other routes in a stack should be able to dismiss back to the anchor. In-app navigation will now always navigate relative to the anchor route.

Code
export const unstable_settings = {
anchor: "index"
}

Pushing Smarter

When pushing routes in a stack, there have historically been three different strategies. We’ve reduced this to two strategies and marked the third as dangerous:

  1. Push a new screen every time. This is useful for content driven apps like social networks where you continuously add to the stack.
  2. Push or pop to the nearest screen. This is useful for books where you may want to pop back to chapter 1 from chapter 2 instead of pushing a duplicate copy.
  3. (Removed) A third behavior was introduced in React Native where you would push a duplicate route by first removing it from the stack and adding it to the top of the stack. This behavior does not have first-class support on iOS and caused issues with UINavigationController that led to freezing. We’ve now removed this behavior by default, but allow it to be dangerously accessed for legacy cases, under the dangerouslySingular flag. We strongly advise against using this behavior as it’s neither ergonomic, nor supported natively.

Additional Improvements

  • Use the new prefetch prop in the <Link /> component to eagerly load a route ahead of time and improve page transition performance. Learn more in the Link docs.
  • Streams API (ReadableStream, WritableStream, TransformStream) is now available on all platforms. Learn more.
  • Added a babel polyfill for import.meta.url syntax to better support libraries on ESM. Learn more.
  • Metro now uses ESM package exports by default. You may need to configure your metro.config.js to support libraries that haven’t migrated yet. Learn more.
  • package.json subpath imports are now supported, you can use these as an alternative to tsconfig paths.
  • Improved support and bug fixes for Tree Shaking in Expo. Learn more
  • Bundling now starts ~4x faster than before with a new lazy crawling architecture in Metro. Bundling no longer slows down in monorepos or projects with ios/android directories generated. This is in addition to the 15x resolver speedup from the last release!
  • Added npx expo serve command for testing production servers locally after running npx expo export
  • Projects using expo-dev-client can now be deployed to TestFlight, enabling you to easily create and share custom Expo Go-like apps with your team.
  • unstable_settings.initialRouteName is now unstable_settings.anchor . The previous name will continue to work for migration purposes.
  • Last but not least—environment variables now support HMR in development meaning you no longer need to restart Expo CLI to see changes!
Fast refresh environment variables

Router v5: Protected Routes Livestream

Mark Lawlor, the father of NativeWind, and essential member of the Expo Router team joined Keith Kurak for a livestream about how to use Protected Routes in Expo Router v5. Protected screens allow you to prevent users from accessing certain routes using client-side navigation. If a user tries to navigate to a protected screen, or if a screen becomes protected while it is active, they will be redirected to the anchor route (usually the index screen) or the first available screen in the stack.

Give this video a watch to see all the different use cases for Protected Routes in Expo Router today:

Expo Router v5 Livestream

Expo Router
React Native
Full-stack development
API Routes
Universal Apps
React Server Components
EAS Hosting
React 19
SDK 53
Server side rendering
Mobile CI/CD
Tree shaking

Get there faster with Expo Application Services

Learn more