[ReactNative] WebViewとアプリでデータをやりとりする

## 前提
- [react-native-webview](https://github.com/react-native-webview/react-native-webview/blob/master/docs/Reference.md)ライブラリを使用
- `react-native`： `0.72.10`

## WebViewで表示している画面の操作をアプリでハンドリングしたい
ネイティブアプリケーションを開発していると、どうしてもWebViewを使用せざるを得ないことって多くないでしょうか？例えば、ECのアプリで決済周りの部分がAPI開発すると色々面倒なので、WebViewでそのまま表示させるとか、スクラッチで作ると工数がかかるから一部分はWebViewにする...などなど。
そんな時に、「WebView内でのイベントをキャッチしてアプリ側でハンドリングしたい」なんてことがしばしばあるかと思います。（モーダル出すとか、アプリの画面に遷移させるとか）
今回はそんな時に、どのようにしてやりとりを行うか説明していきます。

## 流れ
1. [react-native-webview](https://github.com/react-native-webview/react-native-webview/blob/master/docs/Reference.md) の`<WebView />`コンポーネントの[injectedJavaScript](https://github.com/react-native-webview/react-native-webview/blob/master/docs/Reference.md#injectedjavascript) propsでWebView内で実行したいJavaScriptを記述。
1. そのJavaScriptの中で`window.ReactNativeWebView.postMessage()`する。
1. アプリ側では`onMessage={}` propsでWebView内からのpostMessageを受け取る。
1. onMessageでアプリでやりたい処理を記述

## 実装
完成系は以下です。

```SampleScreen.tsx
import React from 'react';
import { StyleSheet, View } from 'react-native';
import WebView, { WebViewMessageEvent } from 'react-native-webview';

export const SampleScreen = () => {
  const injectedJavaScript = `
    window.ReactNativeWebView.postMessage(
      JSON.stringify({
        type: 'postTest',
      })
    );
  `;

  const _onMessage = async (event: WebViewMessageEvent) => {
    const data = JSON.parse(event.nativeEvent.data);
    switch (data.type) {
      case 'postTest':
        console.log('受け取りました。');
        break;
      default:
        break;
    }
  };

  return (
    <View style={styles.container}>
      <WebView
        style={styles.container}
        containerStyle={styles.container}
        source={{ uri: 'https://example.com/' }}
        injectedJavaScript={injectedJavaScript}
        onMessage={_onMessage}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
});

```

### 解説
WebViewコンポーネントの`injectedJavaScript` propsにはWebView内で実行したいJavaScriptを渡してあげます。今回は`window.ReactNativeWebView.postMessage()`として`type`プロパティに`postTest`という文字列を渡してあげます。
```injectedJavaScript.js
const injectedJavaScript = `
    window.ReactNativeWebView.postMessage(
      JSON.stringify({
        type: 'postTest',
      })
    );
  `;
```
このinjectedJavaScriptはWebViewコンポーネントが読み込まれた時に実行されます。もし、実行するタイミングをずらしたいのであれば、`domcontentloaded`や`mutationObserver`、`setTimeout`、クリックイベントなら`.addEventListener('click', function(event) {});`などを使うと良いでしょう。

そして、このpostMessageをアプリ側で受け取るために`onMessage={_onMessage}`として、定義しています。
```onMessage.js
const _onMessage = async (event: WebViewMessageEvent) => {
    const data = JSON.parse(event.nativeEvent.data);
    switch (data.type) {
      case 'postTest':
        console.log('受け取りました。');
        break;
      default:
        break;
    }
  };
```

引数にeventを受け取ることができて、`event.nativeEvent.data`でpostMessageしたデータを受け取ることができます。今回は`type: 'postTest',`なので、`event.nativeEvent.data.type`で'postTest'を受け取ることができます。他にも

```injectedJavaScript.js
const injectedJavaScript = `
    window.ReactNativeWebView.postMessage(
      JSON.stringify({
        type: 'postTest',
        id: 1
      })
    );
  `;
```
とすれば`event.nativeEvent.data.id`としてデータを受け取ることができます。今回のようにswitch-case句で記述すると、他にもinjectedScriptでpostMessageした場合にもハンドリングすることができます。

最後に`case 'postTest'`の中で実行したい処理を記述します。コンポーネントをレンダリングしたり、stateを更新したりできます。

## まとめ
いかがだったでしょうか。個人的にはブラウザでDOM操作していることになるので、ロード時に実行したいJavaScriptがある場合はローディングを挟んだ方がいいかなと思いました。あと、injectScript内で処理が失敗すると、後続の処理はもちろん実行されなくなるので、注意が必要です。

## 参考

https://github.com/react-native-webview/react-native-webview/blob/master/docs/Reference.md

https://zenn.dev/erukiti/articles/react-native-webview-comm