react-native: react native html postMessage can not reach to WebView

I use React Native webview to show index.html, and HTML will post messge to the app. The app will then receive the message and write it to console. The problem is the app cannot receive messages, when postMessage is immediately run on head. I think it maybe related to HTML not finished loading. I then used a delay with setTimeout, and it worked.

Now I want to know:

  • Is there better way to solve this problem?
  • Why the delay 100 milliscond did not work, but delay 200 milliscond did?

I am using React Native version 0.39.0, and Node version 7.2.0.

Here is the code I have so far:

index.html

<head>
<title>Index</title>
<script type="text/javascript" src="index.js"></script>
<script type="text/javascript">
    // can not be received
    postMessage('send to react native from index inline, no delay', '*');

    // can not be received
    setTimeout(function(){
        postMessage('send to react native from index inline, delay 0 milliscond', '*')
    }, 0);

    // can received
    setTimeout(function(){
        postMessage('send to react native from index inline, delay 100 milliscond', '*')
    }, 100);
    
    onload = function() {
        // can not be received
        postMessage('send to react native from index inline after onload, no delay', '*')

        // can received
        setTimeout(function () {
            postMessage('send to react native from index inline after onload, delay 0 milliscond', '*')
        }, 0);
    };
</script>
</head>

index.js

// can not be received
postMessage('send to react native from index.js, no delay', '*');

// can not be received
setTimeout(function() {
    postMessage('send to react native from index.js, delay 100 milliscond', '*')
}, 100);

// can received
setTimeout(function() {
    postMessage('send to react native from index.js, delay 200 milliscond', '*')
}, 200);

React Native web_view_page.js

return (
        <WebView
            source={{uri: 'http://127.0.0.1:8000/index.html'}}
            onMessage={(event) => console.log('onMessage:', event.nativeEvent.data)}/>
    );

Chrome console log

2016-12-21 11:45:02.367 web_view.js:147 onMessage: send to react native from index inline after onload, delay 0 milliscond
2016-12-21 11:45:02.491 web_view.js:147 onMessage: send to react native from index inline, delay 100 milliscond
2016-12-21 11:45:02.628 web_view.js:147 onMessage: send to react native from index.js, delay 200 milliscond

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 30
  • Comments: 47 (2 by maintainers)

Most upvoted comments

Why was this closed? It hasn’t been resolved and the current implementation is flawed requiring complex workarounds.

guys, this works on Android (i haven’t tested it on iOS yet).

  <html>
   <body>
   <script>

   function waitForBridge() {
      
       //the react native postMessage has only 1 parameter
       //while the default one has 2, so check the signature
       //of the function
       
       if (window.postMessage.length !== 1){
         
         setTimeout(waitForBridge, 200);
       }
       else {
         
         window.postMessage('abc');
       }
    }
    
    window.onload = waitForBridge;
    </script>
    </body>
    </html>

You can wait for RN postMessage bridge ready, by running this function once before any window.postMessage call in yours code, tested on RN 0.39

function awaitPostMessage() {
  let isReactNativePostMessageReady = !!window.originalPostMessage;
  const queue = [];
  let currentPostMessageFn = function store(message) {
    if (queue.length > 100) queue.shift();
    queue.push(message);
  };
  if (!isReactNativePostMessageReady) {
    // const originalPostMessage = window.postMessage;
    Object.defineProperty(window, 'postMessage', {
      configurable: true,
      enumerable: true,
      get() {
        return currentPostMessageFn;
      },
      set(fn) {
        currentPostMessageFn = fn;
        isReactNativePostMessageReady = true;
        setTimeout(sendQueue, 0);
      }
    });
  }

  function sendQueue() {
    while (queue.length > 0) window.postMessage(queue.shift());
  }
}

Usage:

awaitPostMessage(); // Call this only once in your Web Code.

/* After you can call window.postMessage many times, and as soon as RN Message Bridge is ready - messadges delivered */
window.postMessage('Once :) ');
window.postMessage('Twice :) ');

Notice: In my code i limit queue for 100 message, fell free to increase this, or remove limit at all.

Can anyone please explain me why is it okay to do these “hacks” instead of just fixing RN’s WebView so it won’t override the global postMessage in a way that it’s breaking the API? What am I missing here? Thanks!

It’s still an issue, just experienced this.

Maybe solve problem, but check for native code in RN: https://github.com/facebook/react-native/blob/master/React/Views/RCTWebView.m#L285 is Wrong, because

String(window.postMessage) === 'function () { [native code] }'

and

String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage') === 'function postMessage() { [native code] }'

And it not equal

But Try THIS code:

function awaitPostMessage() {
  var isReactNativePostMessageReady = !!window.originalPostMessage;
  var queue = [];
  var currentPostMessageFn = function store(message) {
    if (queue.length > 100) queue.shift();
    queue.push(message);
  };
  if (!isReactNativePostMessageReady) {
    var originalPostMessage = window.postMessage;
    Object.defineProperty(window, 'postMessage', {
      configurable: true,
      enumerable: true,
      get: function () {
        return currentPostMessageFn;
      },
      set: function (fn) {
        currentPostMessageFn = fn;
        isReactNativePostMessageReady = true;
        setTimeout(sendQueue, 0);
      }
    });
    window.postMessage.toString = function () {
      return String(originalPostMessage);
    };
  }

  function sendQueue() {
    while (queue.length > 0) window.postMessage(queue.shift());
  }
}

Usage:

awaitPostMessage(); // Call this only once in your Web Code.

/* After you can call window.postMessage many times, and as soon as RN Message Bridge is ready - messages delivered */
window.postMessage('Once :) ');
window.postMessage('Twice :) ');

Notice: In my code i limit queue for 100 message, fell free to increase this, or remove limit at all.

I think for Android you can use window.__REACT_WEB_VIEW_BRIDGE.postMessage instead.

the delay for Android is in executing this:

window.postMessage = function(data) {
  __REACT_WEB_VIEW_BRIDGE.postMessage(String(data));
};

You can use the javascript interface directly instead for Android based on the implementation. However, this could break in a future version if they update it.

Also, just to reiterate.

Placing comments inside the injectedJavaScript property of WebView breaks bridge communication on Android.

So don’t do that I guess 😛.

The react native event listener appears to be delayed after page load. It also seems to be the case that messages posted from the webview using postMessage aren’t received by the webview’s onMessage handler though messages generated by the page inside the webview are received. Perhaps it’s by design but it seems to bear on this issue. Thx to Skarbo for his test.

index.html

<html>
<body>
  <button>Send post message from web</button>
  <div>Post message log</div>
  <script>

    window.setTimeout(function () {
      window.postMessage('send to react native from index inline, delay 2000 milliscond', '*')
    }, 2000);

    document.querySelector("button").onclick = function () {
      logMessage("Sending post message from index.html document..");
      window.postMessage("a message from index.html button", "*");
    }

    document.addEventListener("message", function (event) {
      logMessage("document received " + event.data);
    }, false);

    window.addEventListener("message", function (event) {
      logMessage("window received " + event.data);
    }, false);

    function logMessage(message) {
      console.log((new Date()) + " " + message + "\n");
    }

  </script>
</body>
</html>

App.js

import React, { Component } from "react";
import { Text, View, TouchableHighlight, WebView } from "react-native";
export default class WebViewApp extends Component {
  constructor(props) {
    super(props);

    this.webView = null;
  }

  onMessage(event) {
    console.log("On Message", event.nativeEvent.data);
  }

  sendPostMessage() {
    console.log("Sending post message");
    this.webView.postMessage("Post message from react native");
  }

  render() {
    return (
      <View style={{ flex: 1 }}>
        <TouchableHighlight
          style={{ padding: 10, backgroundColor: "blue", marginTop: 20 }}
          onPress={() => this.sendPostMessage()}
        >
          <Text style={{ color: "white" }}>
            Send post message from react native
          </Text>
        </TouchableHighlight>
        <WebView
          style={{ flex: 1 }}
          source={require("./index.html")}
          ref={webView => (this.webView = webView)}
          onMessage={this.onMessage}
        />
      </View>
    );
  }
}

@Dryymoon

code works but now i am getting below error. any idea?

"Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined"

I can confirm that the solution from @andreibarabas works for me in RN 0.50 under iOS and Android.

To be explicit:

import React, {Component} from 'react';
import {View, WebView} from 'react-native';

export default class WebViewTest extends Component {

  constructor(props) {
    super(props);
    this.state = {
      bridgeJs: `
        (function ready() {
          function whenRNPostMessageReady(cb) {
            if (postMessage.length === 1) cb();
            else setTimeout(function() { whenRNPostMessageReady(cb) }, 100);
          }
          whenRNPostMessageReady(function() {
            postMessage('hi react native!!!');
          });
        })();`
    };
  }

  onMessage(m) {
    alert(m.nativeEvent.data);
  }

  render() {
    return (
      <View style={{height: 0, opacity: 0}}>
        <WebView ref={(wv) => { this.webView = wv; }}
          injectedJavaScript={this.state.bridgeJs}
          onMessage={m => this.onMessage(m)} javaScriptEnabled />
      </View>
    );
  }
}

Thanks!

guys, do you have any idea why a webview post message back and forth in a release build is super slow yet fast in a debug build?

For Android, Dryymoon’s solution works when using the debug version. However, the release version causes the error “__REACT_WEB_VIEW_BRIDGE.postMessage is not a function”

Having the same issue. If a message is sent right when the page is loaded and javascript is executed it fails with that error. However, if it’s executed later it seems to work. Any update on this?

@Dryymoon This method still works. Thank a lot. But there a lot of tricky here 😢

For Android, the release version causes the error “__REACT_WEB_VIEW_BRIDGE.postMessage is not a function”!!!

@willhlaw @vvavepacket I am experience the same issue in production build. Any work around or update on this?

I am using react-native 0.54.2 and react-native-wkwebview-reborn 1.16.0

I second @vvavepacket question. Why does webview post message seem much slower in production than in a debug build (at least in expo comparing development mode (~0.5s) vs production mode (>5s)?

I had the same issue here. I managed to “solve it” as my code needs to be ran only when the user changes page, so I can bind it to window.onbeforeunload, but there definitely needs to be a canonical way to check if the WebView Bridge is ready.