Fixing React Native WebView’s postMessage for iOS
Tags: reactnative, ios, react, javascript
Published: 2019-06-04 20:00:32

In 2016, GitHub user Robert Roskam (raiderrobert) [opened an issue](https://github.com/facebook/react-native/issues/10865) on the React Native repository reporting the error “_Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined_”. In the two years since then, nothing has been done to resolve it within the internal React Native implementation of WebView.

The React Native community has [forked WebView](https://github.com/react-native-community/react-native-webview) specifically to maintain it as a third party package and fix many of these ongoing issues. However, in order to implement these third party packages, you must be able to link React Native packages — `react-native link react-native-webview`. If you are able and willing to do this, your problem is solved. The installation instructions for the community edition of WebView are as simple as:

```
yarn add https://github.com/react-native-community/react-native-webview
react-native link react-native-webview 
```

**Note:** In order to `react-native link ...`, you must first `yarn global add react-native`.

Unfortunately, if you are unable or unwilling to do this, there has simply been no solution to this problem. For years!

Users of Expo, for example, would have to eject their project and write their own native, non-JavaScript implementation of features. Expo will, theoretically, be using these community edition packages in future releases; but with a launch window only weeks away, my team and myself were not willing to wait.

## The Solution 💡

If you care more about getting this solved _right now_ than how it works, this section is for you.

Either `npm install rn-webview` or `yarn add rn-webview` to add the [`rn-webview` package](https://www.npmjs.com/package/rn-webview) to your project.

Wherever you are using `import { WebView } from 'react-native'`, simply replace it with `import WebView from 'rn-webview'`. Then just use the new WebView component as you would the React Native internal implementation, including the use of the `onMessage` prop. The `rn-webview` package is just a wrapper for the internal React Native implementation that intercepts messages through a different channel than the internal `onMessage` prop, but handles it with its own `onMessage` prop, giving the illusion that you are actually using the internal `onMessage` with expected results.

## Caveats 🤕

The `rn-webview` package works by directing `window.postMessage` traffic to `history.pushState` instead. While React Native’s iOS implementation cannot handle `window.postMessage` correctly, it can handle navigation state changes. Because of this, the navigation state change event is the channel through which messages are transferred between the WebView and the native application.

If manipulation of the history state is an important aspect of your application, this solution may not suit your needs. Feel free to [fork the project on GitHub](https://github.com/CharlesStover/react-native-web-view) to offer alternative solutions.

## The Implementation 🔨

### Export 🚢

First and foremost, the `ref` prop of WebView is a particularly important one. Because of this, we don’t want the user to lose access to it. We start the package with a `forwardRef` implementation, where `WebViewPostMessage` is the class name used for this package.

```javascript
export default React.forwardRef((props, ref) =>
  <WebViewPostMessage
    {...props}
    forwardedRef={ref}
  />
);
```

### Render 🎨

The output of this component is going to be the React Native internal implementation of WebView, with a few tweaks. We aren’t going to give it the `forwardedRef` prop, because that is only used to give the parent access to the `ref` and is totally meaningless to the internal WebView. Most importantly, we aren’t going to give it the `onMessage` prop, because that is the source of all of our problems — it’s not supported by iOS!

```javascript
render() {
  const props = {...this.props};
  delete props.forwardedRef;
  delete props.onMessage;

  return (
    <WebView
      {...this.props}
      onNavigationStateChange={this.handleNavigationStateChange}
      ref={this.handleRef}
    />
  );
}

```

We have a custom navigation state change listener, because that is the channel through which we will be listening for messages.

We have a custom ref handler, because we both 1) need access to it inside this component and 2) need to pass the ref back to the parent container via the `forwardedRef` prop.

### Ref 👋

When the internal WebView gives us its ref, we store it on the instance (`this.ref = ref`) for use later. If the parent requested the ref as well, we forward it.

```javascript
handleRef = ref => {
  this.ref = ref;

  // If the caller also wants this ref, pass it along to them as well.
  if (this.props.forwardedRef) {
    this.props.forwardedRef(ref);
  }
};

```

### Inject window.postMessage 💉

Now, a custom implementation of `window.postMessage` needs to exist on any page in the WebView. Whenever the navigation state changes, if it has finished loading, we inject JavaScript into it to override what `window.postMessage` does.

```javascript
handleNavigationStateChange = e => {

  /* We'll do something here later. */

  // If this navigation state change has completed, listen for messages.
  if (
    !e.loading &&
    this.ref
  ) {
    this.ref.injectJavaScript(injectPostMessage);
  }

  /* We'll do something here later. */
};

```

I defined and `importedinjectPostMessage` from a different file for readability.

```javascript
export default `
(function() {
  var EMPTY_STATE = Object.create(null);
  var escape = function(str) {
    return str.replace(/'/g, '\\\\\'');
  };
  var postMessage = window.postMessage;
  window.postMessage = function() {
    if (postMessage) {
      postMessage.apply(window, arguments);
    }
    history.pushState(
      EMPTY_STATE,
      document.title,
      location.href +
      '#window.postMessage(\\\'' +
      escape(arguments[0]) +
      '\\\')'
    );
  };
})();
`;

```

It is an [immediately-invoked function expression](https://en.wikipedia.org/wiki/Immediately-invoked_function_expression) to make sure none of our variables conflict with the web page.

The `EMPTY_STATE` is what is pushed to history, since we won’t be using a state object for our event listener.

The `escape` function escapes apostrophes in a string so that we can place that string in apostrophes. Since the navigation state that we push is not real JavaScript and won’t be passed through any sort of JavaScript interpreter, this step is not exactly necessary. It just allows the state we push to more closely mimic real JavaScript.

The `postMessage` variable checks to see if a `postMessage` function already exists. If so, we’ll want to execute it also during any `window.postMessage` calls.

We define our own `window.postMessage` function. The first thing it does is executes the previous `window.postMessage` function, if it existed.

Next, we push to the history state. We have no state object, so we use the aforementioned empty one. The title of the document is not changing, so we just use the current one. The location of the document is also not changing per se: we are merely appending a hash.

That hash, which we’ll be listening for later, is `window.postMessage('the message')`. It looks like JavaScript, by design, but is not going to be evaluated by any real JavaScript interpreter. We just need a unique hash that won’t collide with real, in-document hashes.

### postMessage Listener 📬

Now that we have our own `window.postMessage` event emitter, we need to listen for it. This is the code that goes at the top of the `handleNavigationStateChange` method.

```javascript
const postMessage = e.url.match(/\#window\.postMessage\('(.+)'\)$/);
if (postMessage) {
  if (
    e.loading &&
    this.props.onMessage
  ) {
    this.props.onMessage({
      nativeEvent: {
        data: unescape(postMessage[1])
      }
    });
  }
  return;
}

```

We check if the new URL matches the `postMessage` hash we defined earlier. If it does, we’re going to `return` so that the rest of the navigation state change event listener doesn’t fire. This is a message event, not a navigation state change (technicalities aside).

Each `postMessage` event will fire the navigation state change twice — once for `loading: true` and one, almost immediately after, for `loading: false`. We are only listening for the `loading: true` event, because it occurs first. The `loading: false` event is ignored, because it is just a duplicate.

Only if the parent component passed an `onMessage` event handler, we call that handler with a mock event that contains the message. We unescape the message before passing it, because we escaped the apostrophes earlier.

The unescape function is defined at the top of the document, because it is constant (does not depend on the instance) and does not need to be a method of the component. You may import it if you prefer to code split it.

```javascript
const unescape = str =>
  str.replace(/\\'/g, '\'');
```

### onNavigationStateChange 🕵

The above covers everything we need for intercepting `window.postMessage` and handling it with one’s own `onMessage` event listener. Our original problem is already solved — `onMessage` works with this WebView. However, since we have overwritten the internal `onNavigationStateChange` listener, the parent is no longer receiving navigation state change events.

At the bottom of the `handleNavigationStateChange` event listener, add the following:

```javascript
if (this.props.onNavigationStateChange) {
  return this.props.onNavigationStateChange(e);
}
return;

```

If the parent has included an `onNavigationStateChange` prop, call it, and give it this navigation state change event.

The empty return is simply personal preference — I don’t believe functions should conditionally return, even if it’s functionally equivalent to an implicit return.

## Conclusion 🔚

As a reminder, you can include the component just outlined by installing the [`rn-webview` package](https://www.npmjs.com/package/rn-webview) from NPM. You may also fork it on GitHub.

If you liked this article, feel free to give it a heart or unicorn. It’s quick, it’s easy, and it’s free! If you have any relevant commentary, please leave it in the comments below.

To read more of my columns, you may follow me on [LinkedIn](https://www.linkedin.com/in/charles-stover), [Medium](https://medium.com/@Charles_Stover), and [Twitter](https://twitter.com/CharlesStover), or [check out my portfolio on CharlesStover.com](https://charlesstover.com/).