July 30, 2025 ยท Expo

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:

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

  1. Create a shared interface in .ts or .tsx that both implementations satisfy.
  2. Place browser mocks, dev-only tooling, and fetch shims in .expoweb.tsx.
  3. Leave the production/native logic in .native.tsx or .tsx.
  4. 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

  1. Run npx expo start --web during development.
  2. Use browser devtools for inspection and time-to-interactive checks for faster development.
  3. Before merging, test on iOS/Android to verify your changes.