Rork apps tend to reach a point where you need to embed a web view — an internal dashboard, a third-party login page, a live docs preview. It works in the simulator, it works in Companion, and then you install a real build on a real phone and the view is completely, silently, blank. I've personally lost half a day to this right before a release, when an Android device wouldn't render anything I threw at it.
What makes WebView failures painful is that the component rarely throws a loud error. Instead it just... stays empty. The good news is the failure modes are a small, well-known set. Here are the six causes I walk through in order whenever a react-native-webview ends up blank in a Rork-generated Expo project.
Before anything else: figure out where it actually stopped
A blank WebView is actually three different failures wearing the same costume:
- The native
WebViewcomponent isn't mounted at all - It's mounted, but the URL failed to load
- It loaded fine, but the HTML body is empty or the SPA is still waiting to render
Telling these apart up front saves an hour of guessing later. The simplest trick is to wire up every relevant event and surface the state on screen.
// components/DebugWebView.tsx
import { WebView, type WebViewNavigation } from "react-native-webview";
import { View, Text, StyleSheet } from "react-native";
import { useState } from "react";
type Props = { uri: string };
export default function DebugWebView({ uri }: Props) {
const [status, setStatus] = useState<"idle" | "loading" | "loaded" | "error">("idle");
const [lastUrl, setLastUrl] = useState<string>("");
const [errorText, setErrorText] = useState<string>("");
return (
<View style={styles.container}>
{/* Dev-only status bar. Hide behind a flag for production. */}
<Text style={styles.status}>
status: {status} / url: {lastUrl}
{errorText ? ` / error: ${errorText}` : ""}
</Text>
<WebView
source={{ uri }}
style={styles.webview}
onLoadStart={(e) => {
setStatus("loading");
setLastUrl(e.nativeEvent.url);
}}
onLoad={() => setStatus("loaded")}
onError={(e) => {
setStatus("error");
setErrorText(e.nativeEvent.description ?? "unknown");
}}
onNavigationStateChange={(nav: WebViewNavigation) => {
setLastUrl(nav.url);
}}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
status: { padding: 8, backgroundColor: "#111", color: "#0f0", fontSize: 12 },
webview: { flex: 1 },
});Drop this in for sixty seconds and you'll immediately see whether the view is stuck on loading, bailed out with error, or reached loaded and is still empty. Everything below assumes you know which bucket you're in.
Cause 1: iOS App Transport Security is blocking HTTP
If the device is an iPhone, onError fires on iOS but the same URL works on Android, you're almost certainly being blocked by App Transport Security. Anything served over http:// is silently refused on iOS — no dialog, no crash, just nothing.
For a Rork-exported Expo project, the clean fix is either HTTPS on the other end, or an allow-list entry in app.json. I strongly prefer the allow-list: setting NSAllowsArbitraryLoads to true tends to raise questions during App Store review, and limiting the exception to one domain passes without friction.
{
"expo": {
"ios": {
"infoPlist": {
"NSAppTransportSecurity": {
"NSAllowsArbitraryLoads": false,
"NSExceptionDomains": {
"staging.example.internal": {
"NSTemporaryExceptionAllowsInsecureHTTPLoads": true,
"NSTemporaryExceptionMinimumTLSVersion": "TLSv1.0",
"NSIncludesSubdomains": true
}
}
}
}
}
}
}Certificate and TLS issues often show up on the API side of the app at the same time, so if you're fighting ATS it's worth sanity-checking your request layer against the Rork network and API request debugging guide.
Cause 2: Android cleartext traffic and mixed content
Android 9 (API 28) disabled cleartext HTTP by default. Same story as iOS: ideally everything is HTTPS, but the reality of internal tools and LAN IPs (http://192.168.x.x) isn't going away.
For an Expo-managed project, flip android.usesCleartextTraffic to true in app.json to get past the network layer. Then, on the WebView itself, set mixedContentMode explicitly. If you don't, an HTTPS page that references HTTP images or iframes will render, but with most of its media silently missing — which reads as a half-empty screen.
<WebView
source={{ uri: "http://192.168.10.5:3000" }}
mixedContentMode="compatibility" // "always" / "compatibility" / "never"
javaScriptEnabled
domStorageEnabled
// Scope this to specific hosts in production builds
/>Don't ship mixedContentMode="always" to production unless you really mean it. The cleaner pattern is an EAS profile-driven env flag that ships compatibility (or tighter) to release builds and opens things up only in dev.
Cause 3: Login loops because cookies and redirects aren't being preserved
Embedding an OAuth or SAML login in a WebView and ending up right back at the sign-in screen — or stuck on a loading spinner — usually comes down to cookies not persisting, or a redirect going to about:blank.
The minimum viable setup I reach for is:
- Set
sharedCookiesEnabledtotrueso iOS shares cookies with its native store - Set
thirdPartyCookiesEnabledtotrueon Android (keep in mind iOS 14+ ITP-style behavior) - Intercept the callback URL scheme in
onShouldStartLoadWithRequestand let native finish the flow
<WebView
source={{ uri: loginStartUrl }}
sharedCookiesEnabled
thirdPartyCookiesEnabled
incognito={false}
onShouldStartLoadWithRequest={(req) => {
if (req.url.startsWith("yourapp://callback")) {
// Detected the custom scheme — hand the URL off to native auth
handleAuthCallback(req.url);
return false; // Don't let the WebView try to load it
}
return true;
}}
/>If you skip this intercept, the WebView hits a URL it can't open and goes blank. When OAuth is the last thing you remember touching and the screen is now white, this is the first place to look.
Cause 4: The page loaded, it just hasn't painted yet
When onLoad fires but the view is still blank, you're usually looking at an SPA that's waiting on something — fonts, requestIdleCallback, an API call — or a viewport issue that's collapsing the layout to zero height.
The fastest way to confirm is to inject a small probe and report back over onMessage.
<WebView
source={{ uri }}
onMessage={(e) => console.log("[webview]", e.nativeEvent.data)}
injectedJavaScriptBeforeContentLoaded={`
window.addEventListener('DOMContentLoaded', () => {
window.ReactNativeWebView.postMessage(JSON.stringify({
ready: document.readyState,
bodyLength: document.body ? document.body.innerHTML.length : 0,
title: document.title,
}));
});
true; // Keeps the RN bridge happy (return value must be undefined)
`}
/>If bodyLength is a healthy number but the screen is still empty, you're probably looking at a CSS height bug (100vh miscomputed), a dark theme painting black text on a black background, or a font that never arrives. If the issue is specifically with missing images, that's a different rabbit hole — I've written it up separately in Rork image and media loading error fixes.
Cause 5: The Android render process died
Since 2023, Android runs the WebView renderer in a separate process. If the OS kills it under memory pressure, or the System WebView is being updated mid-session, the page goes white and doesn't come back. If you don't listen for onRenderProcessGone, your app has no idea anything happened.
The handler is short:
<WebView
source={{ uri }}
onRenderProcessGone={(e) => {
console.warn("[webview] render process gone", e.nativeEvent);
// Auto-reload, or swap to an error fallback UI
webRef.current?.reload();
}}
/>This tends to hit budget Android devices the hardest, and is the source of most "the screen just freezes" complaints in app reviews. It's also one of the cheapest wins — five lines of code and a noticeable drop in bad-review rate. If broader device crashes are also a problem, it's worth pairing this with the approach in debugging Rork apps that crash on real devices.
Cause 6: "It worked in Companion, but TestFlight is blank"
Rork's Companion is generous about certain native settings during development. The surprise usually comes at TestFlight time, when the production build applies the strict configuration your release profile actually has.
The quick audit list:
react-native-webviewis listed inapp.json'spluginsarrayeas.jsondev and production profiles aren't diverging onusesCleartextTrafficorinfoPlistentries- Any ATS exception you added during development is also present in the release profile
The fastest way to confirm whether the problem is Companion-specific is eas build --profile preview --platform ios — a build that mirrors production closely but doesn't require a full TestFlight round trip.
What to do next
A blank WebView feels unfixable because it hides the error, but adding a single layer of instrumentation almost always surfaces the cause within a minute. If you're staring at one right now, drop the DebugWebView snippet above into your screen, watch how status and errorText evolve on a real device, and let that decide which of the six sections is yours.