React NativeをTypeScript + ESLint + Prettier + Storybookで環境構築

2020/12/11に公開
1

はじめに

React Nativeの環境構築をする機会があったので、まとめて紹介します。
やることは以下になります。

  • Expo CLIを使ってReact NativeをTypeScriptで構築する
  • ESLint / Prettier / husky / lint-stagedを導入する
  • Storybookを導入し、Netlifyを利用してPull RequestにコンポーネントカタログのプレビューURLを載せる

完成版: https://github.com/RikutoYamaguchi/react-native-with-linter-and-storybook-sample
PRのサンプル: https://github.com/RikutoYamaguchi/react-native-with-linter-and-storybook-sample/pull/1

Expo CLIでReact NativeをTypeScriptで構築する

ここは公式の手順と全く同じです。
Setting up the development environment · React Native

Expo CLIのインストール

$ yarn global add expo-cli

プロジェクトの作成

$ expo init AwesomeProject
? Choose a template: › - Use arrow-keys. Return to submit.
    ----- Managed workflow -----
    blank                 a minimal app as clean as an empty canvas
>   blank (TypeScript)    same as blank but with TypeScript configuration
    tabs (TypeScript)     several example screens and tabs using react-navigation and TypeScript
    ----- Bare workflow -----
    minimal               bare and minimal, just the essentials to get you started
    minimal (TypeScript)  same as minimal but with TypeScript configuration

(TypeScript) になっているものを選択すれば、TypeScript環境ができあがります。めちゃくちゃ簡単ですね。

blank or tabs or minimal ですが、tabs を選択すると react-navigation が導入され、
かつサンプルとしてタブスクリーンが2つ実装された状態になっています。
実装されたものは結構参考になるなと思ったので、実際に開発をするときは tabs がいいかもしれません。
今回は環境構築のみに絞るので、blank (TypeScript) を選択しました。

以下で、プロジェクトディレクトリに移動しておきます。

cd AwesomeProject

ESLintの導入

初期設定&インストール

以下のコマンドで初期設定&インストールします。

$ npx eslint --init

対話形式で色々聞かれるので、React Native & TypeScriptに適した選択をしていきます。

? How would you like to use ESLint? …
  To check syntax only
  To check syntax and find problems
❯ To check syntax, find problems, and enforce code style

シンタックスチェックと、ファイルの修正も行いたいので一番下の、
To check syntax, find problems, and enforce code style を選択します。

? What type of modules does your project use? …
❯ JavaScript modules (import/export)
  CommonJS (require/exports)
  None of these

JavaScript modules (import/export) を選択します。

? Which framework does your project use? …
❯ React
  Vue.js
  None of these

React を選択します。

? Does your project use TypeScript? › No / Yes

TypeScriptなので、 Yes を選択します。

? Where does your code run? …  (Press <space> to select, <a> to toggle all, <i> to invert selection)
  Browser
✔ Node

Node を選択します。

? How would you like to define a style for your project? …
  Use a popular style guide
❯ Answer questions about your style
  Inspect your JavaScript file(s)

Answer questions about your style を選択します。

? What format do you want your config file to be in? …
❯ JavaScript
  YAML
  JSON

JavaScript を選択します。

? What style of indentation do you use? …
  Tabs
❯ Spaces

Spaces を選択します。
参考: スタイルガイド(コーディング規約) - TypeScript Deep Dive 日本語版#スペース

? What quotes do you use for strings? …
  Double
❯ Single

Single を選択します。
参考: スタイルガイド(コーディング規約) - TypeScript Deep Dive 日本語版#引用符

? Do you require semicolons? › No / Yes

Yes を選択します。
参考: スタイルガイド(コーディング規約) - TypeScript Deep Dive 日本語版#セミコロン

react-hookプラグイン追加

このままだと、フックのルールが適用されないようなので、プラグインを追加します。
フックのルール – React

$ yarn add -D eslint-plugin-react-hooks

.eslintrc.js を修正

以下の修正を行います。

module.exports = {
    'env': {
        'es2021': true,
        'node': true
    },
    'extends': [
        'eslint:recommended',
        'plugin:react/recommended',
        'plugin:@typescript-eslint/recommended'
    ],
    'parser': '@typescript-eslint/parser',
    'parserOptions': {
        'ecmaFeatures': {
            'jsx': true
        },
        'ecmaVersion': 12,
        'sourceType': 'module'
    },
    'plugins': [
        'react',
-       '@typescript-eslint',
+       '@typescript-eslint',
+       'react-hooks'
    ],
    'rules': {
        'indent': [
            'error',
-           4
+           2
        ],
        'linebreak-style': [
            'error',
            'unix'
        ],
        'quotes': [
            'error',
            'single'
        ],
        'semi': [
            'error',
            'always'
-       ]
+       ],
+       'react-hooks/rules-of-hooks': 'error',
+       'react-hooks/exhaustive-deps': 'warn'
-   }
+   },
+   'settings': {
+      'react': {
+          'version': 'detect'
+      }
+   }
};

Prettierの導入

インストール

$ yarn add -D prettier eslint-plugin-prettier

.prettierrc.js を追加

$ touch .prettierrc.js
module.exports = {
  jsxBracketSameLine: true,
  singleQuote: true,
  trailingComma: 'all',
  printWidth: 100,
};

.eslintrc.js を修正

  • pluginsにprettierを追加
...
    'plugins': [
        'react',
        '@typescript-eslint',
-       'react-hooks'
+       'react-hooks',
+       'prettier'
    ],
...
};

ESLintとPrettierの動作確認

package.json を修正

ESLintとPrettierを実行する npm scripts を追加します。

...
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
-   "eject": "expo eject"
+   "eject": "expo eject",
+   "lint": "eslint './src/**/*.{js,ts,tsx}'",
+   "lint:fix": "eslint --fix './src/**/*.{js,ts,tsx}'",
+   "format": "prettier --check ./src",
+   "format:fix": "prettier --write ./src"
  },
...

./src/components/AppButton.tsx を追加

環境構築のテスト用にシンプルなコンポーネントを作ります。

$ mkdir -p src/components
$ touch src/components/AppButton.tsx

./src/components/AppButton.tsx

import React from 'react';
import { Button } from 'react-native';

export type AppButtonPropsType = {
  title: string,
  onPress: () => void
}

export const AppButton: React.FunctionComponent<AppButtonPropsType> = (props: AppButtonPropsType) => <Button title={props.title} onPress={props.onPress} />

ESLintを動かしてみる

$ yarn lint
yarn run v1.22.4
$ eslint './src/**/*.{js,ts,tsx}'

/path/to/your/AwesomeProject/directory/src/components/AppButton.tsx
  9:156  error  Missing semicolon  semi

✖ 1 problem (1 error, 0 warnings)
  1 error and 0 warnings potentially fixable with the `--fix` option.

error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

セミコンロを書いていないのでerrorが発生します。
lint:fix を使ってルールを適用します。

$ yarn lint:fix
yarn run v1.22.4
$ eslint --fix './src/**/*.{js,ts,tsx}'
✨  Done in 0.90s.
import React from 'react';
import { Button } from 'react-native';

export type AppButtonPropsType = {
  title: string,
  onPress: () => void
}

-export const AppButton: React.FunctionComponent<AppButtonPropsType> = (props: AppButtonPropsType) => <Button title={props.title} onPress={props.onPress} />
+export const AppButton: React.FunctionComponent<AppButtonPropsType> = (props: AppButtonPropsType) => <Button title={props.title} onPress={props.onPress} />;

Prettierを動かしてみる

$ yarn format
yarn run v1.22.4
$ prettier --check ./src
Checking formatting...
[warn] src/components/AppButton.tsx
[warn] Code style issues found in the above file(s). Forgot to run Prettier?
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

対象ファイルでwarnが発生しているのでルールを適用します。

$ yarn format:fix
yarn run v1.22.4
$ prettier --write ./src
src/components/AppButton.tsx 164ms
✨  Done in 0.62s.
import React from 'react';
import { Button } from 'react-native';

export type AppButtonPropsType = {
  title: string;
  onPress: () => void;
};

-export const AppButton: React.FunctionComponent<AppButtonPropsType> = (props: AppButtonPropsType) => <Button title={props.title} onPress={props.onPress} />;
+export const AppButton: React.FunctionComponent<AppButtonPropsType> = (
+  props: AppButtonPropsType,
+) => <Button title={props.title} onPress={props.onPress} />;

Prettierのエラー予防

./src ディレクトリ以下に画像ファイルなどを設置するとエラーが発生してしまうので、予防として .prettierignore ファイルを作成します。

$ touch .prettierignore

.prettierignore

*.jpg
*.png
*.gif
*.ttf

必要そうな拡張子を適宜追加しましょう。

huskyとlint-stageを導入する

インストール

$ yarn add -D husky lint-staged

設定

package.json の修正

...
- "private": true
+ "private": true,
+ "lint-staged": {
+   "./src/**/*.{js,ts,tsx}": [
+     "eslint --fix './src/**/*.{js,ts,tsx}'",
+     "prettier --write ."
+   ]
+ },
+ "husky": {
+   "hooks": {
+     "pre-commit": "lint-staged"
+   }
+ }
}

動作確認

一度 ./src/components/AppButton.tsx をルール適用前に戻して、動作するか確認します。

import React from 'react';
import { Button } from 'react-native';

export type AppButtonPropsType = {
  title: string;
  onPress: () => void;
};

export const AppButton: React.FunctionComponent<AppButtonPropsType> = (props: AppButtonPropsType) => <Button title={props.title} onPress={props.onPress} />

$ git add .
$ git commit -m "comment"
husky > pre-commit (node v14.15.1)
✔ Preparing...
✔ Running tasks...
✔ Applying modifications...
✔ Cleaning up...
[master a095dc0] comment
 9 files changed, 5199 insertions(+), 16 deletions(-)
 create mode 100644 .eslintrc.js
 create mode 100644 .prettierrc.js
 create mode 100644 package-lock.json
 create mode 100644 src/components/AppButton.tsx

コミット後以下のようにルールが適用されます。

import React from 'react';
import { Button } from 'react-native';

type AppButtonPropsType = {
  title: string;
  onPress: () => void;
};

export const AppButton: React.FunctionComponent<AppButtonPropsType> = (
  props: AppButtonPropsType,
) => <Button title={props.title} onPress={props.onPress} />;

Storybookの導入

Storybookは公式にReact Native向けのセットアップがありますが、

  • react-native server
  • storybook server

両方を立ち上げないとStorybookでコンポーネントを確認できないようです。

今回は react-native-web のみ確認できればよいと割り切って導入します。

インストール

$ yarn add -D @storybook/react @storybook/addon-essentials babel-loader

※ babel-loaderはStorybookが使用するため必要でした。

設定

$ mkdir .storybook
$ touch .storybook/main.js
$ touch .storybook/webpack.config.js

.storybook/main.js

module.exports = {
  stories: ['../src/**/*.stories.tsx'],
  addons: ['@storybook/addon-essentials'],
};

.storybook/webpack.config.js

module.exports = async ({ config }) => {
  config.resolve.alias = {
    'react-native$': 'react-native-web'
  }
  return config
};

'react-native$': 'react-native-web'react-nativereact-native-web に解決させています。

npm scripts追加

package.json

...
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "eject": "expo eject",
    "lint": "eslint './src/**/*.{js,ts,tsx}'",
    "lint:fix": "eslint --fix './src/**/*.{js,ts,tsx}'",
    "format": "prettier --check ./src",
-   "format:fix": "prettier --write ./src"
+   "format:fix": "prettier --write ./src",
+   "storybook": "start-storybook -p 7007",
+   "storybook:build": "build-storybook -o ./storybookPublic"
  },
...

storybook:build はNetlify上で実行しますがローカルで実行するとコミットにのってしまうので、
.gitignore にアウトプットを追加しておきます。

.gitignore

node_modules/**/*
.expo/*
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/

# macOS
.DS_Store

+storybookPublic/

Storyを追加

Storybookの確認用に簡単なStoryを追加します。

$ touch ./src/components/AppButton.stories.tsx

./src/components/AppButton.stories.tsx

import React from 'react';
import { AppButton, AppButtonPropsType } from './AppButton';
import { Meta, Story } from '@storybook/react';

export default {
  title: 'AppButton',
  component: AppButton,
  argTypes: {
    onPress: {
      action: 'pressed',
    },
  },
} as Meta;

const Template: Story<AppButtonPropsType> = (args) => <AppButton {...args} />;

export const Primary = Template.bind({});
Primary.args = {
  title: 'Button',
};

Storybookを起動

$ yarn storybook

以下の画面がブラウザで表示されます。

StorybookをPull RequestのコンポーネントカタログのプレビューURLを載せる

事前準備

  • Netlifyと連携できるクラウドリポジトリサービスへpush
    • GitHub
    • GitLab
    • Bitbucket
  • Netlifyのユーザー登録をする

Netlifyと連携

1. ログイン後のTeam overview画面の「New site from Git」ボタンをクリックします。

2. ご自身が連携したいサービスを選択します。

※ サービス選択後、はじめてそのサービスをNetlifyで選択した場合、認証が出てくると思います。

3. Pushしたリポジトリを選択します。

4. 以下内容を設定します。

  • Build command: yarn storybook:build
  • Publish directory: ./storybookPublic

5. 設定後「Deploy Site」をクリック

Pull Requestを作ってみる

※ Production Branchに対してのPRしかプレビューができません。

コマンドは GitHubCLI を利用します。

$ git checkout -b feature/preview
$ git commit --allow-empty -m "Preview test"
$ gh pr create --base main
$ gh pr view -w

途中対話型でいろいろ聞かれますがすべてエンターでもPR作成は可能です。
最後のコマンドでPRの画面がWEBブラウザで表示されます。

一番下の「Details」をクリックすることで、Storybookを確認できます。

まとめ

StorybookをNetlifyで配信して、PRから飛べるのは非常に便利で、デザイナー確認などが捗ります。
React Native以外にもこの部分は使い回せるので、ぜひ試してみてほしいです。

Discussion

kondo_scriptkondo_script

いい記事でした〜

prettierのインストール時、yarnがyarnnになっているのでよければ修正お願いします🙏