React Native for WebでHello world

React Native for Web（npmパッケージ名は`react-native-web`）というライブラリを紹介します。

# 概要

https://github.com/necolas/react-native-web

もともとWebの世界（ブラウザで表示するフロントエンドの世界）で生まれたReactを、モバイルのネイティブアプリの世界に広げたものがReact Nativeですが、それを更に発展させて、React Nativeで書いたReactコンポーネントをWebでも動かせるようにするものがReact Native for Webです。一周回ってWebに先祖返りといった感じです。

ネイティブアプリとWebとでReactコンポーネントを共通化できることが一番の利点です。
その需要が今はニッチなのか、ちょっとマイナーなライブラリですが、何気にGitHubにはスターが8000以上ついてます。

有名どころでは [Twitter Lite](https://mobile.twitter.com/)がReact Native for Web（以下、RN Webと略します）を使っています。[^1]
Twitter Liteは最近注目されつつあるProgressive Web App (PWA)であり、モバイル端末上のブラウザ向けに作られています。
今後PWAが本格的に普及してきたら、Twitterのようにネイティブアプリとの二本立ての需要、あるいはネイティブアプリからの移行の需要によって、RN Webは更に脚光を浴びるかもしれません。

この記事では、とっかかりとしてRN WebでHello worldを表示するレベルまでの例を紹介します。

# サンプルプロジェクト

## ディレクトリ構成

今回のサンプルのディレクトリ構成は以下の通りです（細かい部分は省略）。
RNアプリでもあり、RN Webアプリでもあるプロジェクトの例です。
もともとRNアプリだった既存プロジェクトに、あとからRN Webを追加したという想定で紹介します（そういう形でなくても、RN Webは使えます）。

```
/
  node_modules/
  android/
  ios/
  web/
    index.html
    webpack.config.js
  src/
    App.js
  index.js
  index.web.js
  package.json
```

RNアプリなのでandroidディレクトリ、iosディレクトリがあります。アプリの入り口はindex.jsです（これらはこの記事では気にしない部分）。
以下の部分がRN Webのために追加したものです。

- web/index.html
- web/webpack.config.js
- index.web.js

android、iosと並んで、第３のプラットフォームとしてwebがある格好です。

また、いくつか必要なnpmがあるのでインストールしておきます。

```
// reactもdependenciesに必要だが、RNアプリには既にインストールされているはずなので、この例では省略。
npm install --save react-dom react-native-web
npm install --save-dev babel-plugin-react-native-web

// 今回はJavaScriptのバンドラーとしてWebpackを使うので、これらもインストール。
// あくまで今回の例ではWebpackを使うからであり、RN WebにはWebpackが必要というわけではない。
npm install --save-dev webpack webpack-dev-server
```

## ソースコード

HTMLとJavaScriptの内容は以下の通りです。なにせWeb用ですのでHTMLが必要となるわけです。また、アプリの入り口はindex.jsではなくindex.web.jsというRN Web用のものを別途用意します。どちらも数行程度のものです。

```web/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello, world!</title>
  </head>
  <body>
    <div id="root"></div>

    <!-- 今回の例ではWebpackを使い、最終的にbundle.web.jsというファイルを生成する。よってHTMLではそれを読み込む。 -->
    <script src="bundle.web.js"></script>
  </body>
</html>

```

```jsx:App.js
// いたって普通のRNコンポーネント。
// Textなどを、react-native-webではなくreact-nativeからimportしている。

import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

// Hello worldを表示するだけ
const App = () => (
  <View style={styles.box}>
    <Text style={styles.text}>Hello, world!</Text>
  </View>
);

// ちょっとしたスタイルの指定
const styles = StyleSheet.create({
  box: { backgroundColor: '#cccccc' },
  text: { fontWeight: 'bold' }
});

export default App;
```

```index.web.js
import React from 'react';
import { AppRegistry, StyleSheet, Text, View } from 'react-native';
import App from './src/App'

AppRegistry.registerComponent('App', () => App);

// 以下の行がRN Web特有。RN用のindex.jsとの違いはこれだけ。
AppRegistry.runApplication('App', { rootTag: document.getElementById('root') });

```

## ビルド設定

webpack.config.jsの内容は以下の通りです（公式ドキュメントに載っている例を一部簡略化しただけ）。
ポイントは`react-native-web`というBabelプラグインの存在です。これが何者なのかは後述します。
それ以外はさほど特別な点は無く、流し読みしてOKです。

```webpack.config.js
const path = require('path');
const webpack = require('webpack');

const appDirectory = path.resolve(__dirname, '../');

const babelLoaderConfiguration = {
  test: /\.js$/,
  include: [
    path.resolve(appDirectory, 'index.web.js'),
    path.resolve(appDirectory, 'src')
  ],
  use: {
    loader: 'babel-loader',
    options: {
      cacheDirectory: true,
      plugins: ['react-native-web'], // ここがポイント
      presets: ['react-native']
    }
  }
};

module.exports = {
  entry: path.resolve(appDirectory, 'index.web.js'),
  output: {
    filename: 'bundle.web.js',
    path: path.resolve(appDirectory, 'dist')
  },
  module: {
    rules: [
      babelLoaderConfiguration
    ]
  }
}
```

## ブラウザ上で実行

先ほどのWebpack設定のもと、bundle.web.jsを生成してindex.htmlと同じ位置に置き、index.htmlをブラウザで開けばHello worldの画面が表示されます（開発時はwebpack-dev-serverなどを使うとお手軽）。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/188748/a206e325-e288-4bd8-1ed5-82ff3c53f76a.png)

RNアプリと同じ画面をブラウザで表示することができました。
HTMLの中身を見てみると、divタグ（RN Webによって生成されたclass属性がみっちり指定されている）が何層かあって、"Hello, world!"が表示されています。

# RN Webが動く仕組み

RNにView、Textなどのコンポーネントが用意されているのと同様に、RN Webにも同名のコンポーネントが用意されています（一部除く）。
そしてRN WebのViewなどが、内部ではdivなどの具体的なHTMLタグを作るように実装されています。
こうすることで、RNのコンポーネントをWeb用にも使いまわせるようになっています。

ところで先ほどの例のJavaScriptコードでは、importのfromがreact-nativeになっており、react-native-webが登場していません。
なんでこれで動くかというと、先ほど触れた`react-native-web`というBabelプラグイン（npmパッケージ名は`babel-plugin-react-native-web`）のおかげなのです。
このプラグインが、ざっくり言うとimportのfromの部分をreact-nativeからreact-native-webに置換しています。

まとめると、以下の様にしてRN Webは動いています。

1. Babelプラグインによって、RNをimportしている箇所が、RN Webをimportするように置換される。
1. RN Webのコンポーネント（View、Text）などが、最終的にdivなどのHTMLタグとなる。

# 参考記事リンク

[react-native-web はクロスプラットフォームを加速するのか](https://qiita.com/shinout/items/e2e6933d2cc4a5e38e63)

[^1]: GitHubのプロフィールなどを見ると、RN Webの作者であるNicolasさん自身が、Twitter Liteに携わっていました。