Edit this page
Learn how to migrate a website using Expo Webpack to Expo Router.
The original Expo for web version was based on Webpack 4 and focused primarily on building single-page applications (SPAs). This approach was based on Create React App and enabled building simple web apps with Expo SDK and React Native for web.
Expo Router is the new approach to building powerful universal apps that run on web and native. This guide will help you migrate your existing website to Expo Router.
Both React Navigation and Expo Router are Expo frameworks for routing and navigation. Expo Router is a wrapper around React Navigation and has many shared concepts.
@expo/webpack-config
is deprecated and not receiving any new feature updates.
Expo Router supports static rendering on web, which enables search engine optimization (SEO), social media previews, and faster loading times, unlike Expo Webpack. Along with the benefits of React Navigation, it enables automatic deep linking, type safety, deferred bundling, modular HTML templates, static rendering on web, and more.
Expo Router is also designed to fix the main cross-platform issue with Expo Webpack by sharing navigation between web and native without compromising functionality or performance.
Expo Router uses a custom bundler stack based on Metro. It is the same bundler used by React Native. This is great for ensuring maximum code reusability and solves many forked behavior issues from using different bundlers across platforms. This also means certain bundling features may not be available in Expo Router yet.
Ultimately as a full universal framework, Expo Router is a substantially more robust solution than @expo/webpack-config
, which is a bundler integration. It should be used for all new Expo web projects.
Unlike @expo/webpack-config
, Expo Router uses the same CLI commands and features for web and native. Refer to the table below for more information on the differences between Expo Router and @expo/webpack-config
.
Feature | Expo Router | @expo/webpack-config |
---|---|---|
Start command | npx expo start | npx expo start |
Bundle command | npx expo export | npx expo export:web |
Output folder | dist | web-build |
Static folder | public | web |
Config file | metro.config.js | webpack.config.js |
Default config | @expo/metro-config | @expo/webpack-config |
Bundle Splitting | (SDK 50 • web) | |
Global CSS | (SDK 50 • web) | |
CSS Modules | (SDK 50 • web) | |
Static Font Optimization | (SDK 50 • web) | |
API Routes | (SDK 50) | |
Multi-platform | ||
Fast Refresh | ||
Error Overlay | ||
Lazy bundling | ||
Static Generation | ||
Environment Variables | ||
tsconfig.json paths | ||
Tree Shaking | (Partial support) |
In @expo/webpack-config
all routes shared a single HTML file. This file was based on the template in web/index.html
which was then modified by the @expo/webpack-config
to include the necessary scripts and stylesheets.
In Expo Router, there are two different rendering patterns:
web.output: "static"
which outputs a new HTML file for each route in the app. This approach lets you dynamically generate the entire HTML template using the app/+html.js file.web.output: "single"
which outputs a single-page application. This approach lets you use public/index.html
as the template HTML file.In @expo/webpack-config
, you could host static files in the web
directory, which would be served from the website's root. For example, web/favicon.ico
was served from https://example.com/favicon.ico
.
In Expo Router, you can use the public directory to host static files. For example, public/favicon.ico is served from https://example.com/favicon.ico
. Unlike Webpack, Expo Router's hosting works on native too. Make sure to host the files from a server before using them in production.
In @expo/webpack-config
, you could bundle your website for production using npx expo export:web
. This would output a bundle to the web-build directory.
In Expo Router, use the npx expo export --platform web
command to export to the dist directory. You can generate sourcemaps with the --dump-sourcemap
flag. On build, the contents of the public directory will be copied to the dist directory.
Like before, the root babel.config.js file is used for both web and native. You can change the preset by using the platform
property in the API caller:
module.exports = api => {
// Get the platform from the API caller...
const platform = api.caller(caller => caller && caller.platform);
return {
presets: ['babel-preset-expo'],
plugins: [
// Add a web-only plugin...
platform === 'web' && 'custom-web-only-plugin',
].filter(Boolean),
};
};
In Expo Router, all platforms are hosted from the same dev server on the same port. This is convenient for emulating the production behavior of the app. All logs and hot module reloading go through the same port as well.
Due to limitations on native, hosting with fake HTTPS is not currently supported. This feature is less important now than in 2018, as you can test secure features such as camera and location on localhost using a web browser like Chrome.
The expo-constants
library can be used to access the app.json in-app. Behind the scenes, this is accomplished by setting process.env.APP_MANIFEST
with the stringified contents of the app.json file.
In Expo Router, this is done using Babel with the expo-router/babel
in SDK 49 and lower and babel-preset-expo
in SDK 50 and higher. If you modify the app.json, restart the Babel cache with npx expo start --clear
to see the updates.
Experimental functionality. It will be available from SDK 50.
In @expo/webpack-config
, you could bundle your website to be hosted from a subpath by using the PUBLIC_URL
environment variable or the homepage
field in the project's package.json:
{
"homepage": "/evanbacon/my-website"
}
In Expo Router, you can use the experimental baseUrl
field in the project's app.json:
{
"expo": {
"experiments": {
"baseUrl": "/evanbacon/my-website"
}
}
}
Unlike the previous system, this will also update the routing to account for the base path. For example, if you have a route /profile
and you set the base path to /evanbacon/my-website
, then the route will be /evanbacon/my-website/profile
.
See hosting with sub-paths for more information.
In @expo/webpack-config
you could install @pmmmwh/react-refresh-webpack-plugin
and add the following to the webpack.config.js:
const createExpoWebpackConfigAsync = require('@expo/webpack-config');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = async function (env, argv) {
const config = await createExpoWebpackConfigAsync(env, argv);
// Use the React refresh plugin in development mode
if (env.mode === 'development') {
config.plugins.push(new ReactRefreshWebpackPlugin({ disableRefreshCheck: true }));
}
return config;
};
In Expo Router, Fast Refresh is enabled by default using the official Fast Refresh implementation by Meta.
Like @expo/webpack-config
, Expo Router supports generating the favicon.ico file based on the web.favicon
field in the app.json.
warning: Be careful adding service workers as they are known to cause unexpected behavior on web. If you accidentally ship a service worker that aggressively caches your website, users cannot request updates easily. For the best offline mobile experience, create a native app with Expo. Unlike websites with service workers, native apps can be updated through the app store to clear the cached experience. This would be similar to resetting the user's native browser (which they may have to do if the service worker is aggressive enough). See why service workers are suboptimal for more information.
Expo Webpack didn't have built-in service worker support. However, you could add it yourself by using the workbox-webpack-plugin
and adding it to the webpack.config.js.
Workbox doesn't have a Metro integration, but because Workbox doesn't require one of the core features of a bundler (transformation, resolution, serialization), it can easily be used as a post-build step. Follow the guide for using Workbox CLI, and wherever it refers to a "build script" use npx expo export -p web
instead.
For example, here's a possible flow for setting up Workbox. Create a new project with the following command:
-
npm create expo -t tabs my-app
-
cd my-app
Next, create a root HTML file for the app and add the service worker registration script:
import { ScrollViewStyleReset } from 'expo-router/html';
import type { PropsWithChildren } from 'react';
// This file is web-only and used to configure the root HTML for every
// web page during static rendering.
// The contents of this function only run in Node.js environments and
// do not have access to the DOM or browser APIs.
export default function Root({ children }: PropsWithChildren) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
{/* Bootstrap the service worker. */}
<script dangerouslySetInnerHTML={{ __html: sw }} />
{/*
Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
*/}
<ScrollViewStyleReset />
{/* Add any additional <head> elements that you want globally available on web... */}
</head>
<body>{children}</body>
</html>
);
}
const sw = `
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').then(registration => {
console.log('Service Worker registered with scope:', registration.scope);
}).catch(error => {
console.error('Service Worker registration failed:', error);
});
});
}
`;
Now build the app before running the wizard:
-
npx expo export -p web
Run the wizard command, selecting dist
as the root of the app, and the defaults for everything else...
-
npx workbox-cli wizard
-
? What is the root of your web app (that is which directory do you deploy)? dist/
-
? Which file types would you like to precache? js, html, ttf, ico, json
-
? Where would you like your service worker file to be saved? dist/sw.js
-
? Where would you like to save these configuration options? workbox-config.js
-
? Does your web app manifest include search parameter(s) in the 'start_url', other than 'utm_' or 'fbclid' (like '?source=pwa')? No
Finally, run npx workbox-cli generateSW workbox-config.js
to generate the service worker config. Going forward, you can add a build script in package.json to run both scripts in the correct order:
{
"scripts": {
"build:web": "expo export -p web && npx workbox-cli generateSW workbox-config.js"
}
}
Unlike @expo/webpack-config
, Expo Router does not automatically attempt to generate the PWA manifest configuration. You can create one in public/manifest.json:
{
"short_name": "Expo App",
"name": "Expo Router Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
You can link this in your HTML file using the link
tag:
import { ScrollViewStyleReset } from 'expo-router/html';
import type { PropsWithChildren } from 'react';
// This file is web-only and used to configure the root HTML for every
// web page during static rendering.
// The contents of this function only run in Node.js environments and
// do not have access to the DOM or browser APIs.
export default function Root({ children }: PropsWithChildren) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
{/* Link the PWA manifest file. */}
<link rel="manifest" href="/manifest.json" />
{/*
Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
*/}
<ScrollViewStyleReset />
{/* Add any additional <head> elements that you want globally available on web... */}
</head>
<body>{children}</body>
</html>
);
}
If you were using custom bundler plugins, see Expo Metro config for adding custom functionality to your bundler pipeline.
If you used React Navigation for navigating between screens in @expo/webpack-config
, see the migration guide for React Navigation.
See publishing websites and select "Expo Router" on how to deploy Expo Router websites to various hosting providers.