react-native-web対応ノウハウまとめ

# [セットアップ](https://necolas.github.io/react-native-web/docs/installation/)

- `yarn add react-dom react-native-web`
- `yarn add -D babel-plugin-react-native-web`
- `public`ディレクトリを作り、中にファイルを追加
    - `index.html`を作る
    - `index.web.js`を作る
    - `webpack.config.js`を作る
- `package.json`にスクリプトを追加
    - `"web": "webpack serve --mode=development --config ./public/webpack.config.js”`

<details><summary>public/webpack.config.jsのサンプル</summary>

```js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const appDirectory = path.resolve(__dirname, '../');
const babelConfig = require(`${appDirectory}/babel.config.js`);

const compileNodeModules = [
  '@firebase',
  'react-native-vector-icons',
  'react-native-animatable',
  'react-native-gesture-handler',
].map(moduleName => path.resolve(appDirectory, `node_modules/${moduleName}`));

module.exports = {
  entry: path.join(__dirname, 'index.web.js'),
  output: {
    path: path.resolve(appDirectory, 'dist'),
    publicPath: '/',
    filename: 'rnw.bundle.js',
  },
  resolve: {
    extensions: ['.web.tsx', '.web.ts', '.tsx', '.ts', '.web.js', '.js'],
    alias: {
      'react-native$': 'react-native-web',
      'react-native-maps': 'react-native-web-maps',
    },
  },
  module: {
    rules: [
      {
        test: /\.(ts|tsx|js|jsx)$/,
        include: [
          path.resolve(__dirname, 'index.web.js'),
          path.resolve(appDirectory, 'src'),
          ...compileNodeModules,
        ],
        use: {
          loader: 'babel-loader',
          options: {
            ...babelConfig,
            cacheDirectory: true,
          },
        },
      },
      {
        test: /\.svg$/,
        use: ['@svgr/webpack'],
      },
      {
        test: /\.(gif|jpe?g|png)$/,
        type: 'asset/resource',
        parser: {
          dataUrlCondition: {
            maxSize: 8192, // 8KB
          },
        },
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/i,
        type: 'asset/resource',
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, 'index.html'),
    }),
    new CopyPlugin({
      patterns: [
        {
          from: path.resolve(appDirectory, 'public/Fonts/'),
          to: path.resolve(appDirectory, 'dist/Fonts/'),
        },
      ],
    }),
  ],
};
```
</details>

<details><summary>public/index.web.jsのサンプル</summary>

```js
import {AppRegistry} from 'react-native';
import {App} from '../src/App';
if (module.hot) {
  module.hot.accept();
}
AppRegistry.registerComponent('React Native Web', () => App);
AppRegistry.runApplication('React Native Web', {
  initialProps: {},
  rootTag: document.getElementById('app-root'),
});
```
</details>

<details><summary>public/index.htmlのサンプル</summary>

```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>RN Web</title>
    <style>
      #app-root {
        display: flex;
        flex: 1 1 100%;
        height: 100vh;
      }
    </style>
    <link rel="stylesheet" href="./font-style.css" />
  </head>
  <body>
    <div id="app-root"></div>
  </body>
</html>
```
</details>

# [webとそれ以外でコードを分ける方法](https://necolas.github.io/react-native-web/docs/multi-platform/)

- `Platform.OS` を使って分ける方法
    - `import { Platform } from 'react-native';`
- [`Platform.select`](https://reactnative.dev/docs/platform-specific-code)を使って分ける方法
    - `Platform.select({web: ..., default: ...})`
- ファイル名で分ける方法
    - `*.web.js` のように.webがついていれば、webではそれが読み込まれる
    - `MyComponent.tsx`なら、`MyComponent` フォルダを作り、`MyComponent/index.tsx` `MyComponent/index.web.tsx` とファイルを分ける
    - `index.android.tsx` `index.ios.tsx` のようにAndroidとiOS専用にすることもできる

# [Alertが使えない](https://github.com/necolas/react-native-web/issues/1026)

- alertコンポーネントを作り、それを使うように変更する
- ネイティブは`export const alert = Alert.alert` のようにそのままでOK
- web用alert
    - `window.confirm` を使う
    - Modalを使って実装する

# [react-native-firebaseが使えない](https://github.com/necolas/react-native-web/issues/1526) 

- 対応していないので、普通のfirebaseをインストールする
- Uncaught ReferenceError: require is not defined
    - webpack.configの、js,jsx,ts,tsxのincludeに、@firebaseを追加
- webとネイティブで書き方が異なる
    - 共通のインターフェースを用意してfirebaseの機能はそれを使うようにするか、webは名前空間方式(compat)を使うようにする
    - phoneAuth
        - `<View id='recaptcha' />` などを置いて、 [invisible reCAPTCHA](https://firebase.google.com/docs/auth/web/phone-auth?hl=ja#use-invisible-recaptcha)を使う `new RecaptchaVerifier(auth, 'recaptcha', {'size': 'invisible'})`
        - `PhoneAuthProvider.credential` に`verificationId` と認証コードを渡してcredentialを作り、`signInWithCredential` でサインインできる
            - もちろん `confirmationResult.confirm` でもよい
    

# react-native-mapsが使えない

- [react-native-web-maps](https://github.com/react-native-web-community/react-native-web-maps)を入れ、aliasを設定し、index.htmlにスクリプトを追加
- Markerを使おうとすると、[Element type is invalid](https://github.com/react-native-web-community/react-native-web-maps/issues/70)
    - import {Marker} ではなく、import MapViewからのMapView.Markerという古い形式で使わないと表示されない

# [react-native-vector-iconsが使えない](https://stackoverflow.com/questions/60263461/how-to-use-react-native-vector-icons-with-react-native-web)

- webpackのコンパイルする対象にincludeする
- node_moduels/react-native-vector-icons/Fontsをpublicフォルダにコピー
- index.htmlのstyleなどで、`@font-face { src: url(Fonts/Ionicons.ttf); font-family: “Ionicons”; }`
    - `<link rel="stylesheet" href="./font-style.css" />`のように外出しすると見やすい

# [react-navigation](https://reactnavigation.org/docs/web-support/)がうまく表示されない

- 戻るボタンが表示されない
    - Stack.Navigatorの`screenOptions`を設定する
        - `headerBackTitleVisible: true,`
        - `...(Platform.OS === 'ios' && {headerBackImage: HeaderBackImage}),`
            - このHeaderBackImageは自作コンポーネント。vector-iconで「chevron-left」などを指定する

# ローカルパッケージが使えない

- package.jsonで対象パッケージをnohoistにする
- 対象のパッケージを、webpackのコンパイルする対象にincludeする

# レスポンシブ(メディアクエリ)に対応する
- デバイス幅を取得する
    - `useWindowDimensions()`
    - `Dimensions.get('window').width`
- ライブラリを使う
    - [react-responsive](https://github.com/yocontra/react-responsive)を使う
    - 自前のコンポーネントを用意する
<details><summary>responsive.tsxのサンプル</summary>

```tsx
import {useWindowDimensions} from 'react-native';

const mobileWidth = 520;

/**
 * モバイル端末の場合のみ表示する
 * @example
 * ```jsx
 * <Mobile>モバイル端末のみ表示される</Mobile>
 * ```
 */
export const Mobile = ({children}: {children: React.ReactNode}) => {
  const {width} = useWindowDimensions();
  const isMobile = width < mobileWidth;
  return isMobile ? children : null;
};
/**
 * タブレット端末の場合のみ表示する
 * @example
 * ```jsx
 * <Tablet>タブレット端末のみ表示される</Tablet>
 * ```
 */
export const Tablet = ({children}: {children: React.ReactNode}) => {
  const {width} = useWindowDimensions();
  const isMobile = width < mobileWidth;
  return isMobile ? null : children;
};

type Media = 'mobile' | 'tablet';
/**
 * 現在のメディアを取得する
 * @returns {currentMedia: Media} 現在のメディア
 */
export const useCurrentMedia = (): {
  currentMedia: Media;
} => {
  const {width} = useWindowDimensions();
  return {
    currentMedia:
      width < mobileWidth ? 'mobile' : 'tablet',
  };
};
```