Browser-First Expo Development with .expoweb Modules
Metro can prefer dedicated .expoweb files on Expo Web, letting you prototype 80% of a mobile feature inside a browser before touching a device.
Why Split Out .expoweb?
React Native debugging on devices makes simple tasks slow: hover inspection fails, network logging is limited, and crashes are harder to trace. By reserving .expoweb.tsx files for browser-specific implementations you can:
- Swap console-friendly mocks for complex native modules.
- Use desktop tooling (Redux DevTools, Chrome breakpoints) during development.
- Leave native-only code (
.native.tsx) untouched for biometric flows or in-app purchases.
Metro Configuration
Start from expo/metro-config and extend only the parts needed to register .expoweb extensions. Keep the resolver changes minimal so bundle times stay reasonable.
// metro.config.js
const { getDefaultConfig } = require("expo/metro-config");
const defaultConfig = getDefaultConfig(__dirname);
const { resolver: defaultResolver } = defaultConfig;
const expowebExtensions = ["expoweb.tsx", "expoweb.ts", "expoweb.jsx", "expoweb.js"];
const nativeExtensions = ["native.tsx", "native.ts", "native.jsx", "native.js"];
module.exports = {
...defaultConfig,
resolver: {
...defaultResolver,
sourceExts: [
...defaultResolver.sourceExts,
...nativeExtensions,
"svg"
],
assetExts: defaultResolver.assetExts.filter((ext) => ext !== "svg"),
resolveRequest(context, moduleName, platform) {
if (platform !== "web") {
return context.resolveRequest(context, moduleName, platform);
}
// Prioritize .expoweb.* variants before falling back to Expo Web's defaults.
for (const ext of expowebExtensions) {
const request = `${moduleName}.${ext}`;
try {
const result = context.resolveRequest(context, request, platform);
if (result?.type === "sourceFile") {
return result;
}
} catch (_) {
// Ignore lookup failures and continue.
}
}
return context.resolveRequest(context, moduleName, platform);
}
}
};
Authoring Guidelines
- Create a shared interface in
.tsor.tsxthat both implementations satisfy. - Place browser mocks, dev-only tooling, and fetch shims in
.expoweb.tsx. - Leave the production/native logic in
.native.tsxor.tsx. - Write import statements without extensions (
import { Camera } from "./camera";) so Metro negotiates the right file.
What Still Requires Devices
Features backed by platform APIs: biometrics, in-app purchases, push notifications, still need native testing. Ship hooks or facades that fall back gracefully in .expoweb builds and invoke the real module in .native builds.
Testing Workflow
- Run
npx expo start --webduring development. - Use browser devtools for inspection and time-to-interactive checks for faster development.
- Before merging, test on iOS/Android to verify your changes.