第4回 2020年版 既存のウェブサイトに React を追加する

# 1. 概要
Reactを大規模に利用することが難しい場合、少しだけReactを取り入れてみて、徐々に適用範囲を広げていくのが良いです。
Reactでは既存のページに部分的に利用することが可能であり、今回はその方法について説明します。

# 2. 前提条件
## 作業日時
- 2020/3/5

## ソフトウェアのバージョン
| ソフトウェア | バージョン |
|:-:|:-:|
| webpack            | 4.42.0   |
| babel              | 7.8.6    |
| ts-loader          | 6.2.1    |
| react              | 16.13.0  |
| react-dom          | 16.13.0  |
| typescript         | 3.8.3    |

# 3. シンプルな利用方法
## 3.1. 環境の準備
`create-react-app`は利用せず、自分でWebpack等の設定ファイルを準備する。
最初に `yarn init` で `package.json` を作成し、その後、必要なパッケージをインストールする。

webpack、babel、typescriptの他にhtml確認用のwebpack-dev-serverをインストールする。

```console
$ yarn init
$ yarn add -D webpack webpack-cli webpack-dev-server html-webpack-plugin
$ yarn add -D @babel/core babel-loader @babel/preset-env @babel/preset-react @babel/register
$ yarn add -D typescript ts-loader
```
React関連もインストールする。

```console
$ yarn add react react-dom
$ yarn add -D @types/react @types/react-dom
$ yarn add @material-ui/core @material-ui/icons
```

## 3.2.設定ファイルの作成
### 3.2.1.Webpackの設定ファイル
webpackのコンフィグファイルを作成します。

```console
$ touch webpack.config.js
```

設定ファイルには以下のように記述します。
エントリポイントは`index.jsx`で、出力ファイルは`bundle.js`にしています。

```javascript:webpack.config.js
require('@babel/register'); // development.jsでES6を使えるようにする

const path = require('path')

const src  = path.resolve(__dirname, 'src');
const dist = path.resolve(__dirname, 'dist');

module.exports = {
  mode: 'development',
//  mode: "production",

  entry: src + '/index.jsx',
  output: {
    path: dist,
    filename: 'bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.jsx$/,
        exclude: /node_modules/,
        use: [
            {
              loader: "babel-loader", // Babel を利用する
              options: { // Babel のオプションを指定する
                presets: [
                  "@babel/preset-env", // プリセットを指定することで、ES2020 を ES5 に変換
                  "@babel/react" // React の JSX を解釈
                ]
              }
            }
          ]        
      }
    ]
  },
  resolve: {
    extensions: ['.js', '.jsx']
  },
  plugins: []
}
```

### 3.2.2. Typescriptの設定
Typescriptの設定ファイルも作成します。

```javascript:tsconfig.json
{
  "compilerOptions": {
    "sourceMap": true,
    "target": "es5",  /* ｔｓを書くときにどのversionのESが対象か: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
    "module": "es2015" /* どのversionのESを生成するか: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */

    /* Strict Type-Checking Options */
    "strict": true,                        /* すべての strict タイプ・オプション(noImplicitAny, strictNullChecks, noImplicitThis(thisの型チェック), alwaysStrict(strictモードのjs出力)) を 有効化する */
    "noImplicitAny": true,                 /* any型の使用不可 */
    "strictNullChecks": true,              /* nullable型以外でnullを許容しない */

    /* Additional Checks */
    "noUnusedLocals": true,                /* 未使用の変数を許容しない */
    "noUnusedParameters": true,            /* 未使用の変数を許容しない */
    "noImplicitReturns": true,             /* メソッド内で返り値の型があっているかをチェック */

    /* Module Resolution Options */
    "moduleResolution": "node",            /* http://js.studio-kingdom.com/typescript/handbook/module_resolution 参照 */
    "esModuleInterop": true                /* ESModuleと同じ動作をする. */
  }
}
```

## 3.3. サンプルの作成
### 3.3.1. HTML に DOM コンテナを追加する
Reactを追加したい HTML ファイルを用意します。

React で描画したい箇所に空の `<div>` 要素を追加し、ユニークな id 属性を指定します。また、 `<script src="bundle.js" charset="utf-8"></script>` を追加し、React コンポーネントのJavascriptを読み込みます。

これで、後から `<div>` 要素にReact コンポーネントを描画する準備ができました。

```html:index.html
<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="Web site created using create-react-app" />
    <title>React App</title>
</head>

<body><noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="app" />
</body>
<script src="bundle.js" charset="utf-8"></script>
</html>
```
### 3.3.2. Reactコンポーネントを作成する

次にReactコンポーネントを作成します。

```typescript:src/index.tsx
import React from 'react';
import {render} from 'react-dom';

function App() {
    return (
      <div>
      <h1>Hello React!</h1>
      </div>);
}

render(<App/>, document.getElementById('app'));
```

### 3.3.3. ビルド

Webpackを使って上記のコードをビルドします。
成功すれば、 `dist/bundle.js` が生成されます。

```console
$ ./node_modules/.bin/webpack
```
webpack-dev-serverを起動して、ブラウザからlocalhost:8080にアクセスしてみます。
`Hello React!` と表示されれば成功です。

```
$ ./node_modules/.bin/webpack-dev-server
```

# 4. 複数のコンポーネントを利用する方法
先程のサンプルでは一つのRreactコンポーネントを利用しただけですが、次はReactコンポーネントを利用する方法について説明します。

## 4.1. 設定ファイルの修正
複数のファイルを一度に生成できるように、 `webpack.config.js` を修正します。
`entry` に複数のコンポーネントを列挙します。また、`output` の `filename` を `[name].bundle.js`とすることで、それぞれ別のファイルが出力されるようにします。

```javascript:webpack.config.js
require('@babel/register'); // development.jsでES6を使えるようにする

const path = require('path')

const src = path.resolve(__dirname, 'src');
const dist = path.resolve(__dirname, 'dist');

module.exports = {
    mode: 'development',
    //  mode: "production",

    entry: {
        appbar: src + '/index.tsx',
    　  content: src + '/content.tsx'
      },
    
    output: {
        path: dist,
        filename: '[name].bundle.js'
    },
    module: {
        rules: [
            {
                // 拡張子 .ts の場合
                test: /\.tsx?$/,
                exclude: /node_modules/,
                // TypeScript をコンパイルする
                use: [
                    // 下から順に処理される
                    {
                        loader: "babel-loader",
                        // Babel のオプションを指定する
                        options: {
                            presets: [
                                // プリセットを指定することで、ES2020 を ES5 に変換
                                "@babel/preset-env",
                                // React の JSX を解釈
                                "@babel/react"
                            ]
                        }
                    },
                    { loader: "ts-loader" }
                ],
            }
        ]
    },
    resolve: {
        extensions: ['.ts', '.tsx', '.js', '.jsx']
    },
    plugins: []
};
```

## 4.2. サンプルアプリの作成
htmlファイルと、ヘッダとコンテンツのReactコンポーネントの3つのファイルを作成します。

最初にヘッダ部分の描画用のReactコンポーネントを作成する。
`<StylesProvider>` はMaterial-UIで自動生成するクラス名が重複しないようにするため、 `createGenerateClassName` での衝突を避けるためのプリフィックス等の指定を行う。

```typescript:index.tsx
import React from 'react';
import ReactDOM, { render } from 'react-dom';
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles';
import { StylesProvider, createGenerateClassName } from '@material-ui/core/styles';


import {
  AppBar,
  Toolbar,
  Typography,
  IconButton,
  Avatar
} from '@material-ui/core';

import red from '@material-ui/core/colors/red';

// Material-UIアイコン取得
import NotificationImportantIcon from '@material-ui/icons/NotificationImportant';
import MenuIcon from "@material-ui/icons/Menu";

const generateClassName = createGenerateClassName({
  productionPrefix: 'a',
  seed: 'appbar', // classNameが重複しないようにするために設定
});

// スタイルを適用する
const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    headerLogo: {
      color: "inherit",
      marginRight: 20,
    },
    headerTitleStyle: {
      flexGrow: 1,
      color: "inherit",
    },
    menuButton: {
      color: "inherit",
      padding: '8px',
    },
    avatar: {
      margin: '8px',
      backgroundColor: red[500],
    }
  }),
);


function MyAppBar() {
  // CSSを適用する。
  const classes = useStyles();

  return (
    <StylesProvider generateClassName={generateClassName}>  
    <div>
      <AppBar position='static' aria-label="Global Navi">
        <Toolbar>
          <Typography className={classes.headerLogo} variant="subtitle1">My Sample App</Typography>
          <Typography className={classes.headerTitleStyle} variant="subtitle1" >Material UI test</Typography>
          <NotificationImportantIcon></NotificationImportantIcon>
          <IconButton className={classes.menuButton} aria-label="Menu">
            <Avatar className={classes.avatar}></Avatar>
          </IconButton>
          <IconButton aria-label="SideMenu">
            <MenuIcon />
          </IconButton>
        </Toolbar>
      </AppBar>
    </div>
    </StylesProvider>  
  )
}

ReactDOM.render(<MyAppBar />, document.getElementById('appbar'));
```

次にコンテンツ部分の描画用のReactコンポーネントを作成する。

```typescript:content.tsx
import React from 'react';
import ReactDOM, { render } from 'react-dom';
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles';
import { StylesProvider, createGenerateClassName } from '@material-ui/core/styles';

import {
  Typography,
  IconButton,
  Avatar,
  Paper,
  Tab,
  Tabs,
  Box,
  Card,
  CardHeader,
  CardMedia,
  CardContent
} from '@material-ui/core';

import red from '@material-ui/core/colors/red';

// Material-UIアイコン取得
import MoreVertIcon from '@material-ui/icons/MoreVert';

const generateClassName = createGenerateClassName({
  productionPrefix: 'b',
  seed: 'content', // classNameが重複しないようにするために設定
});

// スタイルを適用する
const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    avatar: {
      margin: '8px',
      backgroundColor: red[500],
    },
    card: {
      textAlign: 'center',
      maxWidth: 400,
    },
    media: {
      height: 0,
      paddingTop: '56.25%', // 16:9
    },
    actions: {
      display: 'flex',
    },
    expand: {
      transform: 'rotate(0deg)',
      marginLeft: 'auto',
      transition: theme.transitions.create('transform', {
        duration: theme.transitions.duration.shortest,
      }),
    },
    expandOpen: {
      transform: 'rotate(180deg)',
    },
  }),
);


interface TabPanelProps {
  children?: React.ReactNode;
  index: any;
  value: any;
}

function TabPanel(props: TabPanelProps) {
  const { children, value, index, ...other } = props;

  return (
    <Typography
      component="div"
      role="tabpanel"
      hidden={value !== index}
      id={`simple-tabpanel-${index}`}
      aria-labelledby={`simple-tab-${index}`}
      {...other}
    >
      {value === index && <Box p={3}>{children}</Box>}
    </Typography>
  );
}

function Content() {
  // CSSを適用する。
  const classes = useStyles();
  const [value, setValue] = React.useState(0);

  const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
    setValue(newValue);
  };

  return (
    <StylesProvider generateClassName={generateClassName}>  
    <div>
      <Paper square>
        <Tabs
          value={value}
          indicatorColor="primary"
          textColor="primary"
          onChange={handleChange}
          variant="scrollable"
          scrollButtons="auto"
          aria-label="scrollable auto tabs example"
        >
          <Tab label="TOP" />
          <Tab label="お買い物" />
          <Tab label="ファッション" />
          <Tab label="グルメ" />
          <Tab label="おでかけ" />
          <Tab label="スポーツ" />
          <Tab label="映画・ドラマ" />
        </Tabs>

        <TabPanel value={value} index={0}>
          <Card className={classes.card}>
            <CardHeader
              avatar={<Avatar aria-label="Recipe" className={classes.avatar}>R</Avatar>}
              action={
                <IconButton>
                  <MoreVertIcon />
                </IconButton>
              }
              title="キットカット4種セット(毎日のナッツ＆クランベリー[パウチ36g／ルビー パウチ31g]"
              subheader="2020年3月5日"
            />
            <CardMedia className={classes.media} image="/img/paella.jpg" title="Paella dish"/>
            <CardContent><Typography component="p">カードの説明</Typography></CardContent>
            </Card>
        </TabPanel>
          <TabPanel value={value} index={1}>
            Item Two
        </TabPanel>
          <TabPanel value={value} index={2}>
            Item Three
        </TabPanel>        
        <TabPanel value={value} index={3}>
            Item Four
        </TabPanel>        
        <TabPanel value={value} index={4}>
            Item Five
        </TabPanel>        
        <TabPanel value={value} index={5}>
            Item Six
        </TabPanel>        
        <TabPanel value={value} index={6}>
            Item Seven
        </TabPanel>        
      </Paper>
    </div>
    </StylesProvider>      
      )
    }
    
ReactDOM.render(<Content />, document.getElementById('content'));
```

最後にhtmlファイルを作成する。
ヘッダとコンテンツ用のReactコンポーネントを読み込むため、Reactを描画する<div>にidを指定し、`appbar.bundle.js`と`content.bundle.js`のJavascriptを読み込む。

```html:index.html
<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="Web site created using create-react-app" />
    <title>React App</title>
</head>

<body><noscript>You need to enable JavaScript to run this app.</noscript>
    <header>
        <h1>ページタイトル<h1>
    </header>
    <div id="appbar"></div>
    <div id="content"></div>
    <script src="appbar.bundle.js" charset="utf-8"></script>
    <script src="content.bundle.js" charset="utf-8"></script>
    <footer>
        <nav>
            <a href="index.html">トップページ</a>
            <a href="about.html">このサイトについて</a>
        </nav>
        <p>Copyright 2020</p>
    </footer>
</body>

</html>
```

## 4.3. ビルドする

ビルドして、サーバーで表示します。

```
$ ./node_modules/.bin/webpack
$ ./node_modules/.bin/webpack-dev-server
```

以下の画面が表示されたら成功です。

![既存hamlにReact.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293373/43b6dc08-ea2b-126d-38c8-447e29ec68dc.gif)

# 5. 最後に
今回は既存のhtmlにReactを部分的に利用する方法について説明しました。
既に運用しているサービスがある場合、全面的なSPA化は困難ですが、部分的に置換えていくことで、徐々にReactに移行していくことが可能だと思います。

次回は[Rechartsを利用したデータの可視化](https://qiita.com/tetsurotayama/items/f5eda6dc2bbcd1f71b4e)について説明します。

# 6. 関連記事
Reactに関する記事です。

- [第1回 2020年版 Node.js+Reactのインストール](https://qiita.com/tetsurotayama/items/6dd91dc37941fab462a4)
- [第2回 2020年版 ReactのMaterial UI V4の使い方について](https://qiita.com/tetsurotayama/items/12d6c7b3463246306e87)
- [第3回 2020年版 React+Firebaseでアプリを作成する](https://qiita.com/tetsurotayama/items/179daca2cacb0caad0c5)
- [第5回 2020年版 ReactのRechartsで新型コロナウイルス感染症対策サイトのデータを可視化する](https://qiita.com/tetsurotayama/items/f5eda6dc2bbcd1f71b4e)
- [第6回 2020年版 React+Firebaseで画像のアップロード（その１）](https://qiita.com/tetsurotayama/items/30a0609a0cca5ba5ff0a)
- [第7回 2020年版 React+Firebaseで画像のアップロード（その２）](https://qiita.com/tetsurotayama/items/5129f0cfb21f9ec9b9a0)
- [第8回 2020年版 React+Firebaseで画像のアップロード（その３）](https://qiita.com/tetsurotayama/items/6fb3d3b6e01cac77c85f)
- [第9回 2020年版 ReactにStoryshotsを導入する](https://qiita.com/tetsurotayama/items/dd637e90f4ea008fd878)