CORE JSC

International Technology Partnership

React Native

Fixing React Native WebView postMessage Communication That Silently Stops Working

Messages flow perfectly between the React Native app and the WebView's web content during initial testing, then quietly stop — no error on either side, nothing in the logs. The bridge itself isn't broken; something about a page navigation, a platform difference, or a timing assumption has pulled the two sides out of sync.

Core JSC Team·August 20, 2026
React NativeWebViewpostMessageJavaScript BridgeDebugging

The Problem

A React Native app embeds a WebView and communicates with the web content inside it using window.ReactNativeWebView.postMessage() from the page and the onMessage prop on the RN side (or the reverse direction via injectedJavaScript). During initial development, messages flow correctly in both directions. At some point — often after the WebView navigates to a different route inside itself, or specifically on one platform but not the other — messages from one side simply stop arriving. No exception is thrown, nothing appears in either the RN or the web console, and the failure is easy to mistake for a flaky bridge rather than a specific, findable cause.

Why It Happens

window.ReactNativeWebView is injected asynchronously, and page code that calls it too early fails silently

window.ReactNativeWebView is set up by the WebView component's own bridge initialization, which doesn't necessarily complete before the page's own scripts start running — a script that calls postMessage immediately on load, before that injection has finished, either throws (if the page doesn't guard against it) or, more commonly, is wrapped in an existence check that just quietly does nothing when the object isn't there yet. Either way, RN never sees the message, and nothing about the failure looks like an error from RN's side.

injectedJavaScript runs once at initial load, not on every subsequent in-page navigation

injectedJavaScript executes a single time when the WebView first loads its content. A client-side route change inside a single-page web app running in the WebView doesn't reload the WebView itself, so from RN's perspective nothing happened — but if that injected script was responsible for setting up the page's own message listeners or dispatch logic, and the SPA's own routing lifecycle tore down and didn't correctly reinitialize that setup, communication silently stops from that point on. This looks identical to a WebView bug but is actually a bug in when the setup code runs relative to the page's own navigation.

iOS and Android handle the message channel's payload type differently

The WebView-to-RN bridge is fundamentally string-based, but the two platforms' underlying WebView implementations (WKWebView on iOS, Android's WebView) don't always fail the same way when a non-string value is passed to postMessage — one platform might coerce it or silently drop it where the other happens to work, which is exactly the pattern behind "it works on Android but not iOS" reports for what looks like identical code.

The Fix

1. Guard every postMessage call on the web side with an existence check, and retry briefly if needed

function sendToNative(payload) {
  if (window.ReactNativeWebView) {
    window.ReactNativeWebView.postMessage(JSON.stringify(payload));
  } else {
    // bridge not ready yet — retry briefly instead of failing silently
    setTimeout(() => sendToNative(payload), 50);
  }
}

Never assume the bridge object exists the instant page scripts start executing — a short retry loop (bounded, not infinite) closes the timing gap between page load and bridge injection completing, without needing to guess a fixed delay that might not hold on a slower device.

2. Re-run listener setup on every relevant navigation, not just once via injectedJavaScript

<WebView
  source={{ uri }}
  injectedJavaScript={setupBridgeScript}
  onLoadEnd={() => webViewRef.current?.injectJavaScript(setupBridgeScript)}
/>

If the web content itself is a single-page app whose routing can tear down and not reliably restore the message-handling setup, re-injecting the setup script on relevant lifecycle events (not just the initial load) closes that gap — treating injectedJavaScript as a one-time initializer rather than something that persists automatically across every in-page navigation.

3. Always serialize the payload to a string on both ends of the channel

// web side
window.ReactNativeWebView.postMessage(JSON.stringify({ type: "ready", data }));

// React Native side
<WebView
  onMessage={(event) => {
    const message = JSON.parse(event.nativeEvent.data);
    // ...
  }}
/>

Treating the bridge as string-only on both platforms, and explicitly serializing/deserializing with JSON.stringify/JSON.parse rather than passing objects directly, removes the platform-specific payload-type divergence entirely — this single change is often what fixes an "iOS-only" or "Android-only" postMessage failure.

4. Test both directions on both platforms independently, not just the one used during initial development

Verify RN-to-web and web-to-RN communication separately, and on real iOS and Android devices or simulators rather than just whichever platform happened to be used while building the feature — a failure that only shows up on one platform, or only in one direction, is a strong signal pointing at exactly one of the causes above rather than a general "the bridge is broken."

Why This Works

Each fix closes a specific timing or platform gap rather than treating the symptom as random flakiness. Guarding against an unready bridge object handles the real asynchronous nature of the WebView's own setup; re-injecting listener setup on navigation events accounts for injectedJavaScript genuinely running only once by default; and consistent string serialization removes a platform divergence that has nothing to do with application logic and everything to do with how each native WebView implementation handles the message channel.

Conclusion

WebView postMessage communication that silently stops is almost never a broken bridge — it's a timing assumption (the bridge object not being ready yet), a one-time injection that didn't survive an in-page navigation, or a platform-specific payload-type mismatch. Guard against calling postMessage before the bridge is ready, re-establish listener setup on relevant navigation events rather than relying on a single injection, always serialize messages as strings on both ends, and verify both communication directions on both iOS and Android independently before considering the integration solid.