JAN 23, 2024 by Evan Bacon
Welcome to Expo Router v3, our most powerful release yet! Today we're introducing beta support for the newest Expo platform: Servers. With this, Expo Router is now the first universal, full-stack React framework!
.mjs support.<Link /> components with the new target, push, and className props.Get started with Expo Router v3 today in one line:
npx create-expo-app@latest -t tabs@50
Note: API Routes are still in beta during SDK 50.
API Routes are a zero-config system for creating server endpoints with a unified build process. Adding a +api.js extension to a route will ensure it's only rendered on the server. API routes are hosted from the same dev server as the website and app in development and must be deployed to a dynamic hosting service in production.
import { ExpoRequest, ExpoResponse } from 'expo-router/server';
export function GET() {
return ExpoResponse.json({ hello: 'world' });
}
export function POST(request: ExpoRequest) {
const { prompt } = await request.json();
// Do something with the prompt
return ExpoResponse.json({
/* ... */
});
}
You can export any of the following functions GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS from an API route. Unsupported methods will automatically return 405: Method not allowed.
The new server architecture will be used to render universal React Server Components in an upcoming release.
To better support API Routes, we've added the ability to perform relative fetch requests on native by setting the production server URL in the app.json:
{
"plugins": [
[
"expo-router",
{
"origin": "https://my-app.dev/"
}
]
]
}
This will enable making relative requests with the fetch API, in both development and production environments:
async function fetchHello() {
// Requests from `http://localhost:8081/hello` in development and `https://my-app.dev/hello` in production.
const response = await fetch('/hello');
const data = await response.json();
// Alerts "Hello world"
alert('Hello ' + data.hello);
}
API Routes are matched after standard routes. To account for API Routes, we've added an official convention to match all 404 / Not Found routes. By creating a +not-found.js route you can match all remaining requests after API routes have been processed. This is supported on all native platforms, and web in server-mode. A 404 status code will also be returned on web.
Expo CLI now supports bundle splitting on async imports (e.g. await import("./route")) when bundling for the web platform. Expo Router automatically splits on routes in the app directory and eagerly loads chunks to prevent network waterfalls on initial requests.
Web-only support is available in Expo Router v3. Support for splitting bundles on native platforms will be included with React Server Component support in the future.
You can now change the /app directory to be any directory in your project. This is useful for testing and white-labeling projects with multiple sub-apps.
{
"plugins": [["expo-router", { "root": "./routes" }]]
}
Avoid changing the root directory as this complicates the build process and may cause unexpected development issues. Opt to use the app and src/app directories instead.
Fonts loaded with expo-font are now automatically extracted and preloaded on web when using static or server output. This enables fonts to start loading before the JavaScript has finished, leading to better initial styles. This system also enables you to statically render your app even if there's a top-level render guard.
import { useFonts } from 'expo-font';
export default function RootLayout() {
// `loaded` will be `true` in static websites as the font was eagerly loaded with the HTML before this JS was executed.
const [loaded] = useFonts({
inter: require('@/fonts/inter.ttf'),
});
if (!loaded) {
// This will no longer be called on static web, meaning the entire boundary will be statically rendered to searchable HTML.
return null;
}
return <Stack />;
}
To fix issues with pushing screens in complex routing scenarios, we've changed the router.push() API to always push new routes, whereas the previous version would pop occasionally. You can use the new router.navigate() API to obtain this previous behavior.
We created a set of Jest utilities that could quickly emulate entire navigation structures.
import { renderRouter, screen } from 'expo-router/testing-library';
it('my-test', async () => {
const MockComponent = jest.fn(() => <View />);
renderRouter(
{
index: MockComponent,
'folder/a': MockComponent,
'(group)/b': MockComponent,
},
{
initialUrl: '/folder/a',
}
);
expect(screen).toHavePathname('/folder/a');
});
The Link component now supports target, rel, and download props on web. Link also now has className support which works as-is on web and can be used with tools like Nativewind to add Tailwind support on all platforms.
<Link target="_blank" className="text-blue-300" href="/home" />
Link components currently navigate to the nearest route matching the href prop. You can now force them to always push a new route by passing the new push prop.
// Navigate to the closest route
<Link href="/" />
// Push "/" as a new route
<Link push href="/" />
npx expo export -p web is over 2x faster for static websites. An average v2 project exported in ~23s, v3 exports in ~11s.
The base JS bundle size for production websites is now 30% smaller (from 1.48mb to 1.05mb). The initial bundle size is further decreased by enabling the new bundle splitting functionality on web.
The URL and URLSearchParams standards are built-in. It was previously necessary to polyfill the web standard URL API. We now ship our own implementation in the expo package, enabling removal of duplicate helper libraries and further reducing bundle size.
In Expo Router v3, we've moved the source code and issue tracking to the expo/expo monorepo. During the migration, we fixed and addressed the majority of issues and bugs regarding Expo Router and added lots more documentation and tests.
Configuration requirements like the Babel plugin have been folded into babel-preset-expo and Expo CLI. Additional Expo Router functionality has been integrated across the SDK with packages like Splash Screen, Linking, and Font.
Expo Router is now more powerful, reliable, and seamless than ever before.
server output mode supports server navigation to dynamic routes on web. Previously, you could only perform client-side navigation to routes like app/[id].tsx.react-refresh. The same Fast Refresh implementation now works across all platforms universally and is far more reliable.experiments.baseUrl. You can now deploy static Expo Router websites to GitHub Pages. This API will be stabilized in SDK 51.mailto: and sms: which don't follow the standard :// convention..mjs modules as expected without modifying the metro.config.js.npx expo customize tsconfig.json.expo-yarn-workspaces to enable monorepo support.npx expo export flag --dump-sourcemap has been renamed to --source-maps. Hermes source maps now work more reliably.paths property in your tsconfig.json to add path aliases. For example, "@/*": ["src/*"] will allow you to write code like import Button from '@/components/Button';.expo-router/babel has been removed. Delete this plugin from your babel.config.js file, and be sure to clear the Metro cache before restarting your dev server.router.push default behavior changed. router.push is now router.navigate and the new router.push will always push routes. This is technically a bug fix, but it may cause unexpected changes in complex navigation behavior.react-native-gesture-handler is no longer added automatically. You can now choose to optionally add gesture handler if you wish to use the <Drawer /> navigator.src directory changed to build. We now ship transpiled JavaScript to production in the expo-router/build/* directory. This is a breaking change if you were imported internals from Expo Router.Here's how to upgrade your app to Expo Router v3 from v2:
Upgrade your app to SDK 50: Follow the instructions in the SDK 50 release notes.
Update the babel.config.js:
expo-router/babel plugin in favor of babel-preset-expo preset.module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
// Remove: plugins: ['expo-router/babel']
};
};
npx expo start --clear
router.push is now router.navigate and the new router.push will always push routes.
react-native-gesture-handler is no longer added automatically and must be injected if you wish to use the <Drawer /> navigator.
Enable Async Routes to use the new bundle splitting functionality on web.
If you have a top-level catch-all route like [...missing].js, rename it to +not-found.js if you plan to use API Routes.
If you have custom splash screen handling, change the import of SplashScreen in expo-router to expo-splash-screen.
If you were using the hrefAttrs prop on the Link component for adding additional web props, migrate to top-level props by the same name.
If you're using the react-native-web style escape hatch to set className on Link components for web, migrate to the top-level className prop.
Ensure you use libraries that are versioned to work with Expo SDK 50:
expo@^50.0.0expo-router@^3.0.0react@18.2.0react-native@~0.73.2react-native-web@~0.19.6@react-navigation/native@^6.0.2You can validate versions automatically with Expo CLI:
npx expo install --check
Fetched April 8, 2026