An Expo app's scan screen imports a native-only camera module with no web build, and the web bundle crashes on load; how do you ship a web version?
answer
- the crash is at import, not render
- Cannot find native module
- split the screen into platform files
- getUserMedia behind the web file
- catch ERR_UNAVAILABLE and degrade
basics
~20 sMove the native-only import behind a platform file so the web bundle never loads it, then give ScanScreen.web.tsx a browser path: a camera library with a web implementation, a file input with capture, or a clear 'use the app' fallback.
solid answer
~40 sThe crash happens at **import**: in the browser Expo's `requireNativeModule` throws `Cannot find native module '…'` (a bare module looking up a Turbo Module fails similarly), before the screen renders, so no `Platform.OS` branch inside the component can help. Split the screen: `ScanScreen.tsx` keeps the native camera, `ScanScreen.web.tsx` never imports it, and the route re-exports `ScanScreen`. For the web version, choose by product need: `expo-camera`'s `CameraView` has a web implementation built on the browser's `getUserMedia` (photos come back as base64 URIs), a React DOM file input with `accept="image/*"` and `capture` hands off to the phone's camera app, or a message that scanning needs the app. Wrap platform-specific calls so an `UnavailabilityError` (`ERR_UNAVAILABLE`) degrades the feature, and add a web build to CI so the next native-only import fails the pipeline, not production.
code
tsx · 21 lines// src/features/scan/ScanScreen.web.tsx
import { Text, View } from 'react-native';
import type { ScanScreenProps } from './types';
export default function ScanScreen({ onPhoto }: ScanScreenProps) {
return (
<View style={{ padding: 16, gap: 12 }}>
<Text>Take or choose a photo of the code.</Text>
{/* React DOM element: allowed in a .web.tsx file */}
<input
type="file"
accept="image/*"
capture="environment"
onChange={(e) => {
const file = e.currentTarget.files?.[0];
if (file) onPhoto(URL.createObjectURL(file));
}}
/>
</View>
);
}go deeper
Recall that native-only libraries do not run on the web and that a .web.tsx file can replace a screen for the browser.
Explain why the crash happens at import time and how a platform-file split keeps the native package out of the web bundle.
Walk through diagnosis, the platform-file structure with shared types, the web capture options and their trade-offs, runtime fallbacks and the CI guard.
Decide whether camera capture is a core web feature worth a live preview or a handoff to the app, and set the dependency policy that prevents repeats.
## Reading the crash The web build loads, then fails before the first screen appears, with something like `Cannot find native module 'ExpoAcmeScanner'` or a `TypeError` inside a library's entry file. Two facts explain it: - **The failure is at import time.** Native modules are usually looked up at module scope. `expo-camera` itself does this in `ExpoCameraManager.ts` with `requireNativeModule('ExpoCamera')`. When the screen file imports the library, the lookup runs immediately. - **The web has no native side.** Expo's `requireNativeModule` throws `Cannot find native module '…'` in a browser (during server rendering it returns an empty object instead). Packages that support the web avoid the call there by shipping a `.web.ts` file; a native-only package has none. A bare React Native library that calls `TurboModuleRegistry.getEnforcing` fails because React Native Web does not export `TurboModuleRegistry` at all. So the component's own `if (Platform.OS === 'web')` never gets a chance to run. ## Step 1: take the import out of the web bundle Split the screen into platform files and keep every native-only import in the native file: ```text src/app/scan.tsx export { default } from '@/features/scan/ScanScreen'; src/features/scan/ScanScreen.tsx native: imports the native camera module src/features/scan/ScanScreen.web.tsx web: never imports it src/features/scan/types.ts shared props and result types ``` - The route re-exports the component, so the route exists on every platform and deep links keep working. - Both files export the same component with the same props from `types.ts`. - This is the same pattern Expo's own packages use: `expo-camera` ships `ExpoCameraManager.ts` for native and `ExpoCameraManager.web.ts` for the browser. ## Step 2: choose the web behaviour | Option | What the user gets | Trade-off | |---|---|---| | `expo-camera` `CameraView` | a live camera preview in the page, through the browser's `getUserMedia` | photos return as base64 URIs because browsers have no file paths; needs camera permission and a secure origin | | React DOM `<input type="file" accept="image/*" capture>` | the phone's camera app or a file picker on desktop | no live preview; simplest and most reliable | | "Continue on your phone" screen | a QR code or a link to the app | no web capture at all, but honest | In a `.web.tsx` file you can render the `input` element directly, because React Native Web renders through React DOM. If the scan also decodes barcodes, check that the web path offers it too; the browser's own barcode detection is not available everywhere. ## Step 3: handle what fails at runtime 1. Permission can be denied or unavailable: `expo-camera`'s web permission code throws an `UnavailabilityError` when the browser lacks the permissions API. Catch errors whose `code` is `ERR_UNAVAILABLE` and fall back to the file input. 2. A camera inside a cross-origin iframe renders nothing in Chromium unless the iframe allows `camera`, which the Expo docs call out. 3. Show the web fallback's limits in the UI instead of a broken preview. ## Step 4: stop it happening again - **Build the web target in CI.** A native-only import in shared code then fails the pipeline, not production. - **Check each new dependency's platforms** before it lands in a shared file; Expo's documentation lists supported platforms per SDK package. - **Keep native imports at the edge.** Screens and features import adapters; only platform files import native packages. ## Testing the split - A **unit test per platform file**: render `ScanScreen.web.tsx` in a web-configured test and the native file in the React Native preset, both against the shared props type. - A **smoke test of the web route** in a browser, which is the only place the import-time crash shows up. - A **dependency check in review**: any new import in `ScanScreen.tsx` is fine; any native package imported from a shared or `.web.tsx` file is a blocker. ## What an interviewer is listening for That you locate the failure at import time, fix it structurally with platform files rather than more branches, choose a web behaviour from the product need, and add a build check so the class of bug cannot return.
- Why does the Expo scan screen's web build pass static rendering but crash in the browser?Because `requireNativeModule` returns an empty object when `window` is undefined, which is the case during server or static rendering, and only throws `Cannot find native module` in a real browser. The page renders on the server and then fails when the bundle loads on the client.
- With expo-camera on the web, what form does a captured photo's uri take, and why?A base64 data URI. The Expo docs note that local file system paths are unavailable in the browser, so the web implementation returns the image data inline. Code that uploads the photo must handle a data URI on the web and a file URI on native.
saying these in an interview costs you the question
- Wrapping the camera component in if (Platform.OS !== 'web') fixes the crash.
- The web bundle crashes when the scan screen first renders.
- No Expo camera package can work in a browser.
- requireNativeModule returns undefined in the browser instead of throwing.
- Once the web build works locally, CI does not need a web build step.