#react-native #expo #web #metro #debug

white screen on expo web. the whole bundle died before running

Miuna

happy valentines day. i spent it fighting a white screen.

the Lógica Viva app (Expo SDK 54 + React Native Web) just opened a blank page in the browser. like, nothing. zero. void. the HTML loaded fine (title "Lógica Viva", div #root present), Metro served the JS bundle... but no React content showed up. total white screen. (×_×)

no visible error on the page. just the absolute white staring back at u.

the investigation (with Playwright bc why not)

bc VS Code doesnt have access to the browser console, i used Playwright (headless Chromium) to programmatically capture what was going on. DOM content, JS console errors, everything.

and there came the error:

Cannot use 'import.meta' outside a module

hmm. interesting. and terrible.

root cause: zustand v5 + Metro = silent disaster

zustand v5.0.11 distributes two versions of the code:

Build File Env variable
CJS (CommonJS) middleware.js process.env.NODE_ENV
ESM (ES Modules) esm/middleware.mjs import.meta.env.MODE

zustands package.json has conditional exports. when Metro resolves modules for web platform, it applies the import condition, selecting the .mjs files (ESM).

but Metro generates bundles in classic script format (<script src="...">), and not <script type="module">. in this context, import.meta is a SyntaxError that kills the entire bundle parsing before any code executes. ANY code. React doesnt even get to run. rip.

THE MOST DIABOLICAL PART

Metro compiled without errors. the HTML page returns status 200. but the JS has a parse SyntaxError that happens silently. no visual feedback whatsoever. u just see white. (ꐦ°᷄д°᷅)

when the bundle is broken like this, Metro actually returns JSON with status 500 instead of JavaScript. then the browser refuses to execute bc the MIME type is application/json. genius.

main fix: metro.config.js

i created a metro.config.js to force the react-native export condition (which points to CJS builds) and remove the import condition (which was selecting ESM with import.meta):

const { getDefaultConfig } = require('@expo/metro-config');
const config = getDefaultConfig(__dirname);

// Prioritize 'react-native' (CJS) over 'import' (ESM)
const conditions = config.resolver.unstable_conditionNames;
if (!conditions.includes('react-native')) {
  conditions.unshift('react-native');
}
// Remove 'import' so .mjs files dont get selected
const importIndex = conditions.indexOf('import');
if (importIndex !== -1) {
  conditions.splice(importIndex, 1);
}

module.exports = config;

result: the bundle now uses process.env.NODE_ENV (CJS) instead of import.meta.env (ESM). no more SyntaxError.

plot twist: second error

thought id celebrate... but no. after killing import.meta, this showed up:

supabaseUrl is required.

src/services/supabase.ts called createClient('', '') when env vars EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY werent defined. @supabase/supabase-js throws exception with empty URL, killing module evaluation and preventing React mount. once again. white screen with no explanation. ¯\_(ツ)_/¯

fix: placeholder URL for offline mode.

export const supabase = createClient<Database>(
  supabaseUrl || 'https://placeholder.supabase.co',
  supabaseAnonKey || 'placeholder-key',
  { ... }
);

not functional for real operations, but the app initializes and operates in offline mode.

all the fixes

File What it did
metro.config.js (new) Forces Metro to use CJS builds instead of ESM
app.json "bundler": "metro" in web config
src/services/supabase.ts Placeholder URL/key when env vars missing
src/App.tsx ErrorBoundary + linking config + documentTitle
src/services/notifications.ts Conditional lazy-load of native modules
src/hooks/useSync.ts Lazy-load of NetInfo + navigator.onLine fallback
package.json @expo/metro-config as dependency

verification

ran automated test with Playwright headless and the main screen rendered complete:

Children: 1
Text: " 0 XP / Nv 1 / 0 dias / Daily Review
       Conjuntos e Contagem ..."

HomeScreen with TopBar (XP, level, streak), progress map and bottom nav bar. everything working.

lesson learned

MORAL OF THE STORY

this scenario is a brutal trap bc everything looks ok. Metro compiles without errors, HTML loads with status 200, the #root div exists... but JavaScript dies in parse before executing a single line. and the page gives u no signal.

the only way to detect it was inspecting the browser console (via headless browser). if i had just stared at the white screen without opening console, id still be there now.

moral: if the screen is white and theres no visible error, run a headless browser and capture console errors. and be suspicious of npm package conditional exports, bc they might pick the wrong build for ur bundler. (⌐■_■)

ler em português →