Babel@7で構築するReact開発環境

(2018/10/03 追記）
Babel が正式に v7 リリースされたので、書き直しました。
変更点などは詳しくは[こちら](https://babeljs.io/blog/2018/08/27/7.0.0)で確認してください。

(2020/05/18 追記)
色々古くなったので更新しました。
なお、Facebook社が新しく公開したRecoilについてやReduxについては本記事では触れません。
**TypeScriptベースでのReactの開発環境構築**の内容になります。
また、TypeScript => JavaScriptの変換にはts-loader、JavaScriptのトランスパイルにはBabelを使うように書き直しました。

# 概要
2018年8月28日、ついに Babel の v7 がリリースされました！！！！  
React/Redux の開発環境構築の記事は多いけれども、Babelv7 を使用したものがあまり無いなと感じたので、ここに（備忘録も兼ねて）書き記します。

2017年からこの3年で React で開発する際の周辺モジュールは

- React : 15.4 => 16.13.1
- webpack : 2.4 => 4.43.0
- Babel : babel-preset-2015 => @babel/preset-env
- Linting : TSLint => ESLint

このようにバージョンが上がり、中には使用するものが変わったものまででてきました。
そして、TypeScriptで記述するのがデファクトスタンダートとなっています。
個人的に Babel 周りのパッケージが @babel/package 化されてたり、されてないものもあって設定するときに混乱するので、つらい。。。

なお、TypeScriptを導入すればBabelは必須ではなくなります。
また、BabelからTypeScriptのコードをJavaScriptにトランスパイル(@babel/preset-typescript)することもできるようにもなっています。
一部変換できないコードもありますが、詳しい説明は他の記事に譲ることとします。

# モジュールのインストール
先ずは必要なモジュールをどんどんインストールしていきます。

1.React

```
yarn add react react-dom
```

npmを使っている方は、下記のコマンドを実行してください。

```
npm install react react-dom
```

ついでにcss-in-jsのモジュールをインストールしましょう。
cssModulesを採用する場合はインストールの必要はありません。

```
yarn add styled-components
```

styled-componetsかemotionかはお好みですが、ここではstyled-componentsを採用します。


2. TypeScript

```
yarn add -D typescript
```

styled-componentsを使う場合、下記モジュールもインストールしましょう。


```
yarn add -D 
```

このプラグインを使うと、TypeScriptでstyled-componentsの補完とリンティングがきくようになります。

3.webpack
webpackv4からはwebpack-cliを別途インストールする必要があります。

```
yarn add -D webpack webpack-cli webpack-dev-server
```

ビルドする際のモジュールもインストールします。

```
yarn add -D terser-webpack-plugin fork-ts-checker-webpack-plugin babel-loader ts-loader
```

4.Babel
先にBabelでモノレポ管理されているものをインストールします。

```
yarn add -D @babel/core @babel/preset-env @babel/preset-react @babel/plugin-proposal-object-rest-spread @babel/plugin-proposal-class-properties @babel/plugin-syntax-dynamic-import
```

なお、今まで`babel-preset-stage-n`という Stage Preset がありましたが、これらはv7からは非推奨となっています(package自体はまだ存在しています)。
`@babel/plugin-proposal-object-rest-spread`のように、必要なpluginを各自で別途インストールするように変わりました。  


次に、@babel/package でないbabel系のパッケージをインストールします。


```
yarn add -D babel-plugin-styled-components babel-preset-power-assert
```


5.ESLint 

```
yarn add -D eslint eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks
```

次に、ESLintでTypeScriptを静的解析できるように下記のモジュールをインストールします。

```
yarn add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
```

6.prettier
構文チェックはESLintに任せ、コードフォーマットはprettierに任せます。ここではESLintの設定にprettierのルールをのせることにします。

```
yarn add -D prettier eslint-plugin-prettier eslint-config-prettier
```

7.test関連

```
yarn add -D jest ts-jest jest-fetch-mock eznyme enzyme-adapter-react-16 power-assert testdouble
```

Reactのテストにはjestが標準とされているので、jestでセットアップをしていきます。
power-assertとtestdoubleはお好みでインストールしてください（jestが提供しているAPIでも全然問題はありません。)

(2020/05/18 追記)
現在Reactの公式サイトでは、enzymeではなくtesting-libraryを使うように推奨されています。
enzymeはprops/stateをベースにテストをすることを重きに置いており、testing-libraryはユーザストーリーをベースにテストをすることに重きを置いています。
**歴史を残す意味でenzymeを使用していますが、testing-libraryを導入することを推奨します。**

なお、[MaterialUI](https://material-ui.com/)のようなUI Libraryを作るようであれば、enzymeでのテストをすることを筆者はおすすめします。
何を担保したいかによりますが、testing-libraryだとテストできないようなケースが場合によってはでてきます。

8.型定義ファイル

TypeScriptで補完や型エラーの検出ができるように型定義ファイルをインストールします。

```
yarn add -D @types/enzyme @types/enzyme-adapter-react-16 @types/jest @types/power-assert @types/react @types/react-dom @types/styled-components 
```

# TypeScriptの設定
targetやmoduleは各プロジェクトでターゲットするものを選択してください。

```json
{
  "compilerOptions": {
    "target": "es5",
    "module": "ESNEXT",
    "lib": ["es2020", "dom"],
    "experimentalDecorators": true,
    "jsx": "react",
    "declaration": false,
    "sourceMap": true,
    "noEmitOnError": true,
    "strict": true,
    "downlevelIteration": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "baseUrl": ".",
    "plugins": [
      {
        "name": "typescript-styled-plugin"
      }
    ],
  },
  "exclude": [
    "jest.config.js",
    "test/**/*"
  ]
}


```

# Babelの設定
Babelの設定ファイルはjson,yaml,javascriptなどで記述できますが、javascriptで記述します。


```javascript

const presets = [
    [
        '@babel/preset-env',
        {
            targets: { ie: 11 },
            modules: false,
        },
    ],
    '@babel/preset-react',
];
const plugins = [
    [
        'babel-plugin-styled-components',
        {
            ssr: false,  // ssrする場合はtrue
            minify: true,
            pure: true,
            transpileTemplateLiterals: true,
            fileName: true,
        },
    ],
    '@babel/plugin-proposal-class-properties',
    '@babel/plugin-proposal-object-rest-spread',
    '@babel/plugin-syntax-dynamic-import',
];

module.exports = { presets, plugins };

```

babel-plugin-styled-componentsは、styled-componentsで定義されたコンポーネントに対してdisplayNameを付与したりminifyしたりと詳細な設定を行うことができます。

## babel-plugin-styled-components
例えば、styled-componentsで

```javascript
const Header = styled.header`
    width: 100%;
    height: 60px;
    font-size: 1.5rem;
    text-align: center;
    background-color: #00BCD4;
    color: #FFFFFF;
    z-index: 999;
`;

export default Header;
```

このように定義されたコンポーネントがあったとします。
このHeaderが使用されたコンポーネントをenzymeでテスト際にこのプラグインを使えば

```javascript

const wrapper = shallow(<Hoge />);
assert(wrapper.find('Header').length === 1);
```

findメソッドで定義したコンポーネントの名前でそのまま取得できるようになります。
プラグインを噛まさない場合、Headerと入力しても取得することができません。
displayNameがないのでenzymeでは`[React.Element]`として定義されていたと思います（多分）。

# ESLintの設定
ESLintには厳しめのルールで有名なairbnbを採用します。
また、prettierのルールをESLint側にも書いていますが、eslint-config-prettierの設定をカスタマイズしたい場合はruleプロパティの中に直接書いて変更しましょう。

```javascript
const OFF = 0;
const WARNING = 1;
const ERROR = 2;

module.exports = {
    parser: '@typescript-eslint/parser',
    extends: [
        'airbnb',
        'airbnb/hooks',
        'plugin:@typescript-eslint/recommended',
        'plugin:prettier/recommended',
        'prettier/@typescript-eslint',
        'prettier/react',
    ],
    parserOptions: {
        sourceType: 'module',
        ecmaFeatures: {
            jsx: true,
        },
        project: './tsconfig.json',
        tsconfigRootDir: '.',
    },
    env: {
        jest: true,
        browser: true,
    },
    settings: {
        react: {
            version: 'detect',
        },
        'import/extensions': ['.js', '.jsx', '.ts', '.tsx'],
        'import/resolver': {
            node: {
                extensions: ['.js', '.jsx', '.ts', '.tsx'],
            },
        },
    },
    rules: {
       'prettier/prettier': [ERROR, /* 上書きするprettierの設定 */],
        /* 省略 */
    },
};
```

# webpackの設定
Reactのルートとなるソースコードが`src/App.js`にあるとしてエントリーポイントを設定しています。

```javascript
const TerserPlugin = require('terser-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const path = require('path');

const MODE = process.env.NODE_ENV || 'development';
const PRODUCTION = MODE === 'production';

const PRODUCTION_PLUGIN = [
    new TerserPlugin({
        parallel: true,
        cache: true,
        sourceMap: true,
        terserOptions: {
            output: {
                comments: false,
            },
        },
    }),
]

module.exports => {
    {
        mode: MODE,
        devtool: PRODUCTION ? 'hidden-source-map' : 'source-map',
        entry: {
            bundle: './src/App.tsx',
        },
        output: {
            filename: '[name].js',
            path: path.resolve(__dirname, 'build'),
            chunkFilename: '[name].js',
            publicPath: '/build/',
        },
        resolve: {
            extensions: ['.ts', '.tsx', '.js', '.jsx'],
        },
        module: {
            rules: [
                {
                    test: /\.tsx?$/,
                    use: [
                        { loader: 'babel-loader' },
                        { loader: 'ts-loader', options: { transpileOnly: true } },
                    ],
                },
            ]
        },
        plugins: [
            new ForkTsCheckerWebpackPlugin({
                async: true,
                eslint: true,
                watch: './src/App.tsx',
                tsconfig: './tsconfig.json',
            }),
        ],
        optimization: {
            minimizer: PRODUCTION ?  PRODUCTION_PLUGIN : [],
            splitChunks: {
                cacheGroups: {
                    vendor: {
                        test: /react|react-dom/,
                        name: 'vendor',
                        chunks: 'initial',
                        enforce: true,
                    },
                },
            },
      },
      performance: {
          hints: false,
      },
};
```

また、npm scriptnの方にこのように設定しています。

```json
"scripts": {
    "start": "webpack-dev-server --mode development --inline --history-api-fallback --no-info --open",
    "release": "NODE_ENV=production webpack --mode production"
}
```

## webpack-serveについて
webpack-dev-serverの後継モジュールとしてwebpack-serveがあります。
公式ページよりwebpack-serveを使うようにアナウンスされていましたが、2018年9月18日に再びwebpack-dev-serverで更新が行われるようになりました。
なんだったんだお前は・・・

***webpack-serve は現在 DEPRECATED になっています***


# Reactコンポーネントの作成
ReactでHello Worldを表示できるようにコンポーネントを作成します。

```tsx:src/App.tsx
import React from 'react';
import ReactDom from 'react-dom';
import { Root } from './components/Root';

ReactDom.render(
    <Root title="Hello World" />,
    document.getElementById('content'),
);
```

このApp.tsxが起点となります。
続いてRoot.tsxを作ります。

```tsx:src/components/Root.tsx
import React from 'react';

type Props = {
    title: string;
}

export const Root:React.FC<Props> = ({ title }) => (
    <div>{title}</div>
);

```


ここまでの設定が終わりましたら、`npm start`で開発サーバを起動すると画面にHello Worldが出力されていると思います。
これで、開発環境の設定は終わりました！


# test環境の構築
最後に、test環境を構築していきます。
構築といっても、テストファイルは作らず設定ファイルのみ作成します。

```javascript:jest.config.js
const dir = __dirname;

module.exports = {
    transform: {
        '^.+\\.tsx?$': 'ts-jest',
    },
    setupFiles: ['./test/setup.js'],
    testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
    coveragePathIgnorePatterns: [
        '<rootDir>/node_modules/',
    ],
    resolver: null,
    moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
};

```

jestの設定ファイルをルートのパスに作成し、TypeScriptで記述されたファイルをテスト対象にふくめます。
また、enzyme等の設定ファイルはtest/setup.jsに別途記述し、それを読みこむようにします。　

```javascript:test/setup.js

const React = require('react');
const fetch = require('jest-fetch-mock');
const Enzyme = require('enzyme');
const Adapter = require('enzyme-adapter-react-16');

Enzyme.configure({ adapter: new Adapter() });
global.fetch = fetch;

// React.memo, React.Suspenseを実行できるようにする
jest.mock('react', () => {
    const r = jest.requireActual('react');
    const Suspense = ({ children }) => `<div>${children}</div>`;
    Suspense.displayName = 'Suspense';

    return { ...r, memo: x => x, Suspense };
});

```

2019年ごろまではenzymeがReact.memo,React.SuspenseなどのAPIに対応しておらず、モックをして強制的に動くようにしています。
Suspenseはただのテキストが返されるなど不完全な対応をしていますが、今だと対応して動くようになっているかもしれません（未検証ですみません、、、）

テストする際のスクリプトは

```json

"scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
}
```

testフォルダ配下のファイルをテストするように設定しています。

# 終わり
設定は以上になります！