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.
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
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. (⌐■_■)