React + TypeScript + ESLint + Storybook

# React + TypeScript + ESLint + UIテスト環境の構築

## 初めに

この記事は[Aizu Advent Calendar 2021 10](https://adventar.org/calendars/6636)日目の記事です。

## 概要
React/TypeScriptにESLintを導入するする際、色々とこけることが多かったので書きました。また、今回UIテストにはStorybookを使用しますが、使い方までは説明しないので[公式](https://storybook.js.org/docs/react/get-started/introduction)などを見てください。

## 前提
- npmが使える
    - ターミナルで`npm -v`を実行して`8.1.4`などの出力がされればOK
    - されなかった場合はnpmの環境構築をしておいてください

## 動作環境

- 機種:MacBook Air (M1 2020)
- OS:MacOS ver 11.2.3
- npm:ver 8.1.4
- node:ver 17.2.0

## React/TypeScript

まずReactとTypeScriptのプロジェクトを作ります。今回使う`create-react-app`はプロジェクトの名前のディレクトリを生成するため、先にプロジェクト用のディレクトリを作っておく必要はありません。またこのことから、GitHub上でリポジトリを作る場合PrivateとPublicの選択だけして、READMEの生成などは行わない方が良いと思います。

### create-react-app
まずプロジェクトのディレクトリを配置したい場所へ移動してください。移動し終えたら、下記のコマンドを`プロジェクト名`の部分だけ変更して実行しましょう。

```sh
$ npx create-react-app プロジェクト名 --template typescript --use-npm
```

> `create-react-app`はReactの環境を自動でビルドしてくれます。
> `プロジェクト名`はその名の通りあなたの作りたいプロジェクトの名称にしましょう。ただし、英大文字を使えないそうなので、日本語などは無理だと思います。記号だと、`-`は使えましたが他は試していないので調べてみてください。
`--template typescript`はその環境をTypeScriptのものにしてくれます。
最後に`--use-npm`ですが、これは後ほど説明します。


> 追記

> ```sh
> create-react-app A
> Cannot create a project named "A" because of npm naming restrictions:
> 
>   * name can no longer contain capital letters
>
> Please choose a different project name.
> ```
> 
> ```sh
> create-react-app あ
> Cannot create a project named "あ" because of npm naming restrictions:
> 
>   * name can only contain URL-friendly characters
> 
> Please choose a different project name.
> ```

上のコマンドを実行したら、`cd プロジェクト名`で作られたディレクトリに移動してください。

## ESLint
### 初期設定
まず、お使いのエディターで`package.json`を開き、

```diff:package.json
-  "eslintConfig": {
-     "extends":[
-       "react-app",
-       "react-app/jest"
-     ]
-   },
```
この部分を削除してください。次にターミナル上で、以下を実行してください。

```sh
$ npm install eslint --save-dav
```
これで、このプロジェクトに`eslint`のパッケージがインストールされます。とはいえインストールされてもまだ使えないので、初期設定のために以下を実行してください。

```sh
$ npx eslint --init
```
実行するといくつかの質問をされるので以下のように答えてください。

| |質問内容 |選択 |
|---|---|---|
|1 |How would you like to use ESLint? | To check syntax, find problems, and enforce code style |
|2 |What type of modules does your project use? |JavaScript modules (import/export) |
|3 |Which framework does your project use? |React |
|4 |Does your project use TypeScript? |Yes |
|5 |Where does your code run? |複数選択が可能です、Browserは選択しておいてください |
|6 |How would you like to define a style for your project? |Use a popular style guide |
|7 |Which style guide do you want to follow? |Airbnb: https://github.com/airbnb/javascript |
|8 |What format do you want your config file to be in? |JSON |
|9 |Would you like to install them now with npm? |Yes |

> 長くなりそうなので、ここでは解説しません。

これでプロジェクト内にeslintに必要なパッケージ群がインストールされ、`.eslintrc.json`という、以下のようなファイルが生成されたと思います。

```json:.eslintrc.json
{
    "env": {
        "browser": true,
        "es2021": true
    },
    "extends": [
        "plugin:react/recommended",
        "airbnb"
    ],
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true
        },
        "ecmaVersion": 13,
        "sourceType": "module"
    },
    "plugins": [
        "react",
        "@typescript-eslint"
    ],
    "rules": {
    }
}
```

これで`npx exlint src/* --fix`などと実行すればESlintがsrc内のコードのうち修正可能な部分を自動整形してくれます。ただ、毎回これを打つのは手間なので、`package.json`内の`scripts`にスクリプトを追加します。

```package.json
{
    ...,
    "scripts": {
      "lint": "eslint \"src/**/*.{ts,tsx}\" \"src/*.{ts,tsx}\" --fix",
      ...
    },
    ...
}
```

こうすることで先程のコマンドを`npm run lint`で実行できます。

実行するとおそらく大量のエラーが出たことでしょう。これを今から修正していきます。

### ESLintの設定

では、私が手元で`npm run lint`を実行して出たエラーを上から順に潰していきたいと思います。

#### 1. `error  Unable to resolve path to module './App'`

以下のコマンドを実行してください

```sh
$ npm install eslint-import-resolver-typescript --save-dev
```

したら、`.eslintrc.json`に以下を追記してください

```.eslintrc.json
{
  "settings": {
    "import/resolver": {
      "typescript": {}
    }
  },
  ...
}
```

#### 2. `error Missing file extension "tsx" for "./App"`

`.eslintrc.json`の`rules`内に以下を追加

```.eslintrc.json
{
  ...
  "rules": {
    "import/extensions": [
      "error",
      "ignorePackages",
      {
        "ts": "never",
        "tsx": "never"
      }
    ]
  }
}
```

#### 3. `error  'test' is not defined`

`.eslintrc.json`に以下のように追加。

```.eslintrc.json
{
  env: {
    "jest/globals": true,
    ...
  },
  ...
  plugins: [
    "jest",
    ...
  ],
  ...
}
```

#### 4. `error  JSX not allowed in files with extension '.tsx'`

`.eslintrc.json`に以下のように追加。

```.eslintrc.json
{
  ...
  "rules": {
    "react/jsx-filename-extension": [
      "warn",
      {
        "extensions": [
          ".tsx"
        ]
      }
    ],
    ...
  }
}
```

### 追加の設定

これで全てのエラーが消えたかと思います。ここからは私の好みが含まれますがより良い開発のためにいくつか設定していこうと思います。また、設定していることを前提としてやっていくので今回は設定しておいてください。

#### 1. ファンクションコンポーネントをアロー関数で表記するようにする

`.eslintrc.json`に以下のように追加。

```.eslint.json
{
  ...
  "rules": {
    "react/function-component-definition": [
      2,
      {
        "namedComponents": "arrow-function",
        "unnamedComponents": "arrow-function"
      }
    ],
    ...
  }
}
```

`App.tsx`などでエラーが出ますが`npm run lint`を実行すれば修正されます

#### 2. 関数の返り値の型を明示的に記入させる

`.eslintrc.json`に以下のように追加。

```.eslint.json
{
  ...
  "rules": {
    "@typescript-eslint/explicit-function-return-type": [
      "error",
      {
        "allowExpressions": true
      }
    ],
    ...
  }
}
```

`App.tsx`と`reportWebVitals.ts`でエラーが起きていると思うので修正します。

```App.tsx
import React from 'react';
import logo from './logo.svg';
import './App.css';

const App: React.VFC = () => (
  ...
```

```reportWebVitals.ts
import { ReportHandler } from 'web-vitals';

const reportWebVitals = (onPerfEntry?: ReportHandler): void => {
  ...
```

#### Prop Typesに関するルールをオフにする

`.eslintrc.json`に以下のように追加。

```.eslint.json
{
  ...
  "rules": {
    "react/prop-types": "off",
    "react/require-default-props": "off",
    ...
  }
}
```

これでESLintの設定は終わりです。一応ここまでで出来上がった`.eslintrc.json`を載せておきます。設定の順番が異なっていたりしてもあまり気にしなくて大丈夫です。

```.eslintrc.json
{
  "settings": {
    "import/resolver": {
      "typescript": {}
    }
  },
  "env": {
    "jest/globals": true,
    "browser": true,
    "es2021": true
  },
  "extends": [
    "plugin:react/recommended",
    "airbnb"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaFeatures": {
      "jsx": true
    },
    "ecmaVersion": 13,
    "sourceType": "module"
  },
  "plugins": [
    "react",
    "@typescript-eslint",
    "jest"
  ],
  "rules": {
    "react/prop-types": "off",
    "@typescript-eslint/explicit-function-return-type": [
      "error",
      {
        "allowExpressions": true
      }
    ],
    "react/function-component-definition": [
      2,
      {
        "namedComponents": "arrow-function",
        "unnamedComponents": "arrow-function"
      }
    ],
    "react/jsx-filename-extension": [
      "warn",
      {
        "extensions": [
          ".tsx"
        ]
      }
    ],
    "import/extensions": [
      "error",
      "ignorePackages",
      {
        "ts": "never",
        "tsx": "never"
      }
    ]
  }
}
```

## Storybook

以下のコマンドを実行してください

```sh
$ npx sb init
```

途中で

```
? Do you want to run the 'eslintPlugin' fix on your project?
```

と聞かれるので`y`と入力。

すると、`src`ディレクトリ内に`stories`ディレクトリが生成されます。これで、`npm run storybook`と実行すればStorybookが起動するのですが、このディレクトリ内に生成されたファイルはESLintによってエラーを吐いてしまうのでStorybookが途中で落ちます。今回は参考用に`button.css`、`Button.tsx`、`Button.stories.tsx`、`Introduction.stories.mdx`を残してそれ以外は消してください。

ではこれらのファイルのエラーを無くします。

#### 1. `error '@storybook/react' should be listed in the project's dependencies, not devDependencies`

`package.json`の`devDependencies`内から`"@storybook/react": "^6.4.9",`を消し、同様の内容を`dependencies`内に追記してください。`"^6.4.9"`の部分は変わっているかもしれないので生成されていた方を使うようにしてください。

#### 2. `error Prop spreading is forbidden`

該当する行の上に

```js
// eslint-disable-next-line react/jsx-props-no-spreading
```

と書き込んじゃってください。行儀が悪いですが、Storybook用の型がよくわからないのでこれでどうにかします。

#### 3. `error  propType "primary" is not required, but has no corresponding defaultProps declaration`

interfaceごと消してしまってください。代わりに、以下のように型を宣言してください。

```ts
type ButtonProps = {
  label: string,
  primary?: boolean,
  backgroundColor?: string,
  size?: 'small' | 'medium' | 'large',
  onClick?: () => void
}
```

そして、Buttonの型宣言を修正します。

```Button.tsx
...
const Button: React.VFC<ButtonProps> = ({
  primary = false,
  size = 'medium',
  backgroundColor,
  label,
}) => {
  ...
```

ついでに引数からpropsを消しちゃったので、以下のようにpropsを消します。

```tsx
    <button
      type="button"
      className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
      style={{ backgroundColor }}
    >
```

#### 4. `error  Prefer default export`

`Button.tsx`のButtonの宣言についている`export`を外して、ファイルの一番下に`export default Button`を追記。

```Button.tsx
import React from 'react';
import './button.css';

...

const Button: React.VFC<ButtonProps> = ({
  ...
};

export default Button;

```

これによってButton.stories.tsxでButtonをインポートしている部分を修正しないとなのでします。

```Button.stories.tsx
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';

import Button from './Button';

...
```

これにてStorybookの設定完了です。`npm run storybook`と実行してみてください。Storybookが立ち上がるはずです。

これにてReact + TypeScript + ESLint + Storybookの環境構築完了です。お疲れ様でした、そして頑張ってください。