Expo Router v3 beta is now available

Evan Bacon
Exposition
Published in
9 min readDec 12, 2023

--

The third major release of Expo Router is now in beta for the next month. Expo Router v3 is part of Expo SDK 50 (React Native 0.73.0).

Expo Router v3 offers a new experimental system for building server endpoints: API Routes, reduced bundle size, and more powerful web support––we’re still on track to deliver full web support in v4.

The full release notes for Expo Router v3 won’t be available until the stable release, but you can browse the changes in the expo/expo CHANGELOG to learn more about the scope of the release and any breaking changes.

New Features

  • 2x faster static website exports: npx expo export -p web is over 2x faster. An average v2 project exported in ~23s, v3 exports in ~11s. These savings scale with the project!
  • 30% smaller base bundle size: The base 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.
  • API Routes: Based on our API Routes RFC––This is 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. Learn more in API Routes. This is a very large feature and will remain experimental even after the v3 stable release.
  • Bundle Splitting: Expo CLI now supports bundle splitting on async imports when bundling for web platform. We’ve added this to Expo Router to automatically split on routes and eagerly load chunks to prevent network waterfalls. This also works with static rendering (SSG). Learn more in Async Routes. This was the last major blocker for adding universal React Server Components to Expo.
  • Configurable app directory: While not recommended, you can now change the /app directory to be any directory in your project. Learn more about the root directory.
Changing the root directory is now possible in Expo Router v3
  • Testing Library: To provide robust test coverage for Expo Router, we created a set of Jest utilities that could quickly emulate entire navigation structures. This testing library is now available for public consumption. Learn more in Testing Expo Router.
  • URL Support: We’ve added built-in support for the standard URL API and updated all of our APIs to use this, leading to a net bundle size reduction on all platforms, especially web. This is part of a larger effort to improve data fetching across Expo. Learn more about the URL API.
  • Not Found 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 anything that’s unmatched in a directory. This is supported on all native platforms and web in server-mode. When this route is matched, a 404 status code will also be returned on web. Learn more about +not-found.
  • New router.navigate API: To fix issues with pushing, 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. `Link` components currently use the navigate behavior by default still, but you can change this to use push by passing the new push prop. e.g. <Link push href=”/” />
  • Dynamic Routes on web: The new 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 but the server API is capable of redirecting requests to any route in your project automatically. This is not supported with standard static output.
  • New Link Props: 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.
  • Universal Fast Refresh: We’ve fixed universal Fast Refresh upstream so you no longer need resolutions on react-refresh. The same Fast Refresh implementation now works across all platforms universally and should be substantially more stable!
  • Base URL Support: Expo Router now support deploying to subdomains with experiments.baseUrl — this applies to all platforms so you may want to configure it with an environment variable in app.config.js. With the addition of this feature, you can now deploy static Expo Router websites to GitHub Pages. We plan to stabilize this API in SDK 51.
  • Auto Font Optimization: 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 also enables you to statically render your app even if there’s a top-level render guard. Learn more.
A before-and-after illustration of fonts loading immediately with HTML when loaded via expo-font in Expo Router v3, where they once did not load until the Javascript had been executed.
An example of fonts loading immediately with HTML when loaded via expo-font in Expo Router v3, where they once did not load until the Javascript had been executed.
  • Improved Tailwind/PostCSS on web: PostCSS with Expo’s Metro web will no longer be blocked on caching. This means you can use full Tailwind + PostCSS on web and integrate with fantastic UI packages like Shadcn UI (web-only). Metro CSS is now enabled by default!
  • Support for external links: You can now link to popular external URLs like mailto: and sms: which don’t follow the standard :// convention of other URLs.
  • Added mjs and cjs support. All modules are converted to commonjs in the bundler as ESM is not supported on native, but you can now import mjs modules as expected.
  • Custom Metro resolvers and transforms: Users can now extend the Metro resolver and modify the transformer using the Babel caller, enabling better control over the bundling process. Learn more in the new Expo Metro docs.
  • Improved Typed Routes: Typed routes ensure better stability over time by automatically generating TypeScript types for your project. You can now generate types in CI with npx expo customize tsconfig.json
  • tsconfig paths enabled by default: tsconfig/jsconfig path aliases are now enabled by default and follow the TypeScript resolution more closely, they’re also now supported in jest-expo.
  • Better Source Maps: Source map exports in production web are now supported, we’ve renamed the npx expo export flag --dump-sourcemap to --source-maps — Hermes source maps now work more reliably.
  • Improved monorepo support: Projects no longer need expo-yarn-workspaces to enable monorepo support in their app. The majority of standard monorepo functionality is built-in to Expo CLI and Expo Metro Config.

Breaking Changes

  • Expo Webpack is deprecated in favor of our universal Metro for web.
  • Expo Router v3 only works with Expo SDK 50. Ensure you upgrade your project before upgrading Expo Router.
  • The deprecated useSearchParams and useLink hooks have been removed.
  • Default CSS reset has been updated to match the latest react-native-web@0.19.8 recommendation. You may need to update your local template or +html styles if you were depending on styles from the previous reset.
  • React Native Gesture Handler is no longer added automatically. If you use the Drawer navigator or any React Native Gesture Handler features, you’ll need to add the global provider to the Root Layout. We recommend avoiding this dependency on web platforms as it will increase bundle size substantially and mostly be unused on web.
  • The SplashScreen export is now deprecated, and all the features have been moved upstream to expo-splash-screen
  • Remove expo-router/babel babel plugin in favor of babel-preset-expo
  • If you have a root [...missing].tsx route, rename this to +not-found.tsx otherwise API routes will not be available. Normal routes, including catch-alls, have higher priority over API routes and not found routes.
  • Expo Router no longer has client-side mocks for __dirname and __filename globals.
  • Remove deprecated <Screen /> prop redirect in favor of the <Redirect /> component which works more reliably. We still plan to improve redirection in the upcoming versions.
  • expo-head native module is now part of expo-router and should not be installed separately.
  • The source for expo-router now lives in build instead of src — if you were dangerously importing nested modules, update them to use expo-router/build/*

Bug Fixes

We’ve been actively fixing bugs and improving stability with Expo Router. The majority of notable fixes have all been backported to v2 and released progressively throughout the previous release-cycle.

Expect improved TypeScript support, more stable redirection, smaller bundle sizes, and better web support.

Using the Beta

  • Initialize a new project with SDK 50 beta:
    npm:
    npx create-expo-app --template tabs@beta
    yarn: yarn create expo-app --template tabs@beta
    Note: create-expo-app will install dependencies with npm when npx is used and yarn when yarn create used.
  • Upgrade an existing project:
    Install the beta version of the Expo package:
    npm install expo@next or yarn add expo@next
    Upgrade all dependencies to match SDK 50:
    npx expo install --fix
  • Install the latest Expo Go for iOS to your physical device:
    Use this TestFlight open beta link and follow the instructions.
  • Install the latest Expo Go for iOS simulators or Android emulators/physical devices:
    Launch your project through Expo CLI (press the i or a keyboard shortcut after running npx expo start) and the updated version of Expo Go will be automatically installed.
  • SDK 50 beta is not yet available on Snack.
  • Read the documentation by selecting it from the version selector in the API reference section.

Migrating from Expo Router v2 to v3

  • Ensure you remove any Yarn resolution or NPM overrides especially for metro, metro-resolver, and react-refresh. If you skip this, then Fast Refresh may not work as expected.
  • Possibly the largest behavior change in any version of Expo Router––router.push is now router.navigate and router.push always pushes routes. This is technically a bug fix, but it may cause unexpected changes in complex navigation behavior.
  • Remove the expo-router/babel preset in favor of babel-preset-expo and be sure to clear the Metro cache before restarting your dev server—this means running npx expo start --clear or npx expo export --clear
  • If you have a top-level [...missing].js route, ensure you rename this to +not-found.js if you plan to use API Routes. Otherwise, you’ll only see the “not found” route when you ping an API endpoint.
  • react-native-gesture-handler is no longer added automatically and must be injected if you wish to use the drawer navigator. We recommend avoiding this dependency on web platforms as it will increase bundle size substantially and mostly be unused on web. Learn more Drawer navigator.
  • Enable Async Routes on web to use the new bundle splitting functionality on web. Async Routes may have issues on Android with Reanimated, you can disable the feature per-platform if needed.
  • 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, e.g. hrefAttrs={{ target: '_blank' }} should now be target="_blank" — this applies to target, rel, download — all of which are web-only and automatically shimmed on native.
  • If you’re using the react-native-web style escape hatch (style={{ $$css: true, _: “myclass” }}) to set className on Link components for web, migrate to the top-level className prop, e.g. <Link className="myclass" />

How to report issues

Thank you for helping us with testing the release — we look forward to shipping it soon! 🚀

--

--