React を TypeScript で使う際のツール考察 2018 春

最近のフロントエンドの流れから取り残されている感じがしたので、一念発起して React で小さなアプリを作ろうと思いました。
せっかくなので、 React 関連ツールはなるべく統合して使うようにし、コード本体は TypeScript を使って開発しようと設定を始めました。
( webpack 4 が出てきてしまいましたので、まだ周回遅れです。)

残念ながら、 create-react-app でテンプレートを作成してからツールを追加していくたびにエラーに見舞われたので、メモ書きとして記録しておきます。
執筆に長い期間かかってしまいましたので、もしその間にライブラリがアップデートされ、動かなくなっていたら申し訳ありません。

目次と使用ツール

(以下のリンクは関係する部分へジャンプします。)

Windows 10 Pro (Fall Creators Update), Yarn, WebStorm で動かしています。

TL;DR

  • webpack 設定ファイル( dev )は各ツールで共有して使うと楽になる。
  • 生成されるファイルは ‘.gitignore’ & ‘tsconfig.json’ を指定して、ビルドのチェックの対象外にする。
  • 各ツールの設定ファイルは ‘tsconfig.json’ で exclude に加える。
    (気になるのであれば、設定ファイルの方を整える。もしくは ESLint でチェックするようにする。)
  • GitHub の Issues のコメント欄で解決策が唐突に書き込まれているものも多い。
  • 最終結果(全行程+掃除)

create-react-app

まずは、公式チュートリアルなどでお馴染みの、 create-react-app を使ってサクッと React のテンプレートを作成します。
似たような多数のボイラープレートが存在しています。( JavaScript ベスト・オブ・ザ・イヤー 2017 - Reactボイラープレート
TypeScript バージョンのプロジェクトを生成する方法はすでに用意されていますので、それを利用します。
https://github.com/wmonk/create-react-app-typescript

$ create-react-app my-app --scripts-version=react-scripts-ts

依存パッケージを全部インストールするので、ちょっと時間がかかります。
終わると、だいたいこんな感じになります。

ここであまり説明しても仕方がないですが、この状態で yarn start を実行するとサーバー http://localhost:3000 とブラウザが立ち上がり、コンパイルなどの後に「 Welcome to React 」のページが表示されます。
yarn test を実行すると、 Jest による単体テストが実行されます。 yarn build では、プロダクションビルドが ‘./build’ に生成されます。これに対して、出力結果のように serve -s build とすることで、プロダクションビルドされたページを http://localhost:5000 に表示させることができます。この場合、 “React Dev Tools” を使って詳しい確認を行うこともできます。
“start” も “test” も Watch モードで起動するため、コードを書きながら逐次自動実行させることができます。

さて、「 Jest で」などと書きましたが、 ‘package.json’ の中にはそれらしいパッケージの名前は見当たりませんし、そもそも webpack などを含めて設定ファイル自体が見当たりません。
実際には、 “react-scripts-ts” というライブラリが必要なことを全部やってくれています。何も気にせず開発を進めていけるのは良いことですが、これは逆にツールを追加していく上では邪魔になる場合が出てきます。

そこで、 “eject” という非可逆動作のコマンドが用意されています。 yarn run eject を実行すると、 “react-scripts-ts” が消え、 “react-scripts-ts” に頼らず構築した場合の設定ファイル群が生成されます。
だいたいこんな感じになります。
( ‘package.json’ の jest.globals.ts-ject.tsConfigFile が絶対パスで出力されるので、それは相対パスに変えています。 )

webpack

この記事を閲覧する方には釈迦に説法かもしれませんが、 webpack は、いわゆる昔の Browserify のようなモジュールバンドラとしてよく使われています。
記事公開時の最新バージョンは webpack 4 なので違いがあるかもしれませんが、【意訳】Webpackの混乱ポイントを読んでおくと迷わず済むと思います。

ツール導入のメインではありませんが、統合のための基盤として webpack のローダーに少し手を加えていきます。
ファイルに対する処理をチェーンしていく感覚は gulp と似ているような雰囲気をなんだか感じます。(個人の感想)

Clean Code

この章では、コードをきれいに保つためのツールを設定します。

TSLint

TSLint は、 TypeScript 向けのスタイルチェックツールです。

基本設定

“eject” することで、 ‘package.json’ のあるディレクトリに ‘tslist.json’ が出現しますが、このファイルを使って設定を行います。
特に何もしなくても、 “start” や “build” で自動で検査が行われます。もちろん、手動で行っても構いません。

まず、 “eject” 後は “start” の出力に Missing baseUrl in compilerOptions というエラーが出るので、これを直しておきます。
これは、同じく出現した、 TypeScript 設定ファイルである ‘tsconfig.json’ の compilerOptions.baseUrl. を指定しておきました。

tsconfig.json
     "suppressImplicitAnyIndexErrors": true,
- "noUnusedLocals": true
+ "noUnusedLocals": true,
+ "baseUrl": "."
},

“eject” の後では React 用のルールしか設定されていませんので、 tslint:latestextends の最初に追記します。
これで TypeScript のコードはある程度はチェックが効くようになります。

tslint.json
   "extends": [
+ "tslint:latest",
"tslint-react"

(※ ESLint に対して xo というオールインワンツールがあり、 TypeScript については tslint-xo というルールセットは配布されていますが、この記事では使っていません。)

tslint:latest を追加したことにより新しいスタイルエラーが報告されてきますが、設定用スクリプトまで検知しています。
これを ‘tsconfig.json’ の excludeconfig を追加することにより除外します。(本当はそちらも整えた方がいいですが…)
TSLintでJavaScriptをLint のように JavaScript 専用の設定もできるはずです。

tsconfig.json
     "jest",
- "src/setupTests.ts"
+ "src/setupTests.ts",
+ "config"
]

ここまでの変更により、プロジェクトはだいたいこんな感じになります。

型定義ファイルの用意

TSLint による警告をすべて修正しようすると、このままでは進めなくなります。

App.tsx 警告行
4
const logo = require('./logo.svg');

Node 開発の JavaScript の場合、このような警告が出た場合は次のように修正するのが自然でしょう。

App.tsx 修正案
4
import logo from './logo.svg';

ただし、これはそのままでは (4,18): Cannot find module './logo.svg'. として阻まれます。
外部リソースということから、 <img src={logo} ...<img src="logo.svg" ... として .svg ファイルをコピーするというのもシンプルで良いでしょう。
この記事では、 .d.ts ファイルを使うことによって、この問題を回避します。

端的に言えば、 Unable to import svg files in typescript - Stack Overflow により解決できますので、この方針で設定を進めます。
まず、 ‘tsconfig.json’ の compilerOptions.rootDir の代わりに compilerOptions.rootDirs を使って ["src", "typings"] を指定することで、型定義ファイルの場所を追加指定します。(参考:TypeScript 2.0のModule Resolution Enhancementsについて - Qiita
これに合わせて ‘typings’ ディレクトリを作成します。実際は型定義ファイルの置き場はどこでもいいですが、今回は専用のディレクトリを用意して分離してしまいます。

tsconfig.json
     "moduleResolution": "node",
- "rootDir": "src",
+ "rootDirs": [
+ "src",
+ "typings"
+ ],
"forceConsistentCasingInFileNames": true,

そして、 .svg ファイルのための TypeScript 型定義ファイル(!)を準備します。リンク先のものを、 TSLint が通るように少し修正しています

typings/svg.d.ts
1
2
3
4
declare module '*.svg' {
const content: string;
export default content;
}

これにより、先ほどの修正案を正しくコンパイルできるようになりました。おそらくこちらのほうが、 JavaScript で React コンポーネントを実装するの場合に近くなると思います。
TSLint の警告をすべて解消するとこんな感じになるはずです。

Prettier

先ほどまでの過程で、 TSLint でバグの発生しやすい書き方に警告がでるようになりました。
しかし残念ながら、読みやすさに関しては大した変更を加えてはくれません。

そこで、 Prettier というツールを導入します。が、そのままでは TSLint と整形結果が衝突する可能性があるので、ちょっとした設定が必要です。
ESLint の場合は ESLint(あるいはTSLint)とPrettierを併用する知見 が詳しいので、これを TSLint 向けの参考にします。
幸いにも、同様のツールで TSLint に対応するものが tslint-config-prettiertslint-config-prettier に存在していますので、それを利用します。

まずは、パッケージを追加します。

$ yarn add -D prettier tslint-plugin-prettier tslint-config-prettier

続いて、 ‘tslint.json’ の編集を行います。
extends の最後に tslint-config-prettier を追加し、 rulesDirectory["tslint-plugin-prettier"] として加えます。

tslint.json
 {
+ "rulesDirectory": [
+ "tslint-plugin-prettier"
+ ],
"extends": [
"tslint:latest",
- "tslint-react"
+ "tslint-react",
+ "tslint-config-prettier"
],

最後に Prettier 本体の設定を ‘tslint.json’ に加えていきます。 rule の最後に "prettier": true を追加します。
ESLint のルール設定でよく記述するように、 true でなく [true, { "singleQuote": true }] のようにしても構いません。
ただし、エディタ拡張などが ‘.prettierrc’ ( Prettier 設定ファイル)のほうを読み、表示や整形の面で面倒になる可能性があるので、今回は分けて記述します。

tslint.json
       "check-typecast"
- ]
+ ],
+ "prettier": true
}
.prettierrc
1
2
3
4
{
"singleQuote": true,
"semi": true
}

Prettier を導入して .ts(x) ファイルを整形すると、こんな感じになります。
New rule: jsx-space-before-trailing-slash · Issue #125 · palantir/tslint-react を見る限り、残念ながら Prettier による整形と少し競合する状態が残っているようです。

CSS Modules with TypeScript

続いて、 CSS Modules を TypeScript で利用してみることにします。これにより、 CSS セレクタをすべてタイプセーフに書けるようになります。
本記事の執筆時点では、 2 種類のライブラリが見つかりました。

Plan 1

一つ目は、 TypeScript + React JSX + CSS Modules で実現するタイプセーフなWeb開発 - Qiita を利用した方法です。
これは API ライブラリと CLI ツールですが、別の作者が webpack loader を作成しているので、これを利用します。
webpack では、 css の設定ツールに新たに加える形で設定します。

Plan 2

二つ目は、 typings-for-css-modules-loader を使う方法です。
やってくれることは Plan 1 とほぼ同じです。 webpack では css-loader を置き換える形で設定します。
"namedExport": true"camelCase-option": true を設定しない場合、 CSS ファイル内の kebab-case の識別子は出力されないようなので注意。)

今回は、 Typescript doesnt find the typings のような問題が双方で発生するため、 CLI でも実行できる Plan 1 により設定していくことにします。

まずはパッケージのインストールから始めます。

$ yarn add -D typed-css-modules typed-css-modules-loader

続いて ‘package.json’ にコマンドを追加します。
前述のような処理順番の問題のために、 ‘src’ 以下にある CSS ファイルから ‘typings’ へ .css.d.ts ファイルを生成するような共通の設定を書き、それを各プロセスの前に入れています。

package.json
   "scripts": {
+ "tcm": "tcm ./src -c -o ./typings",
+ "prestart": "yarn run tcm",
"start": "node scripts/start.js",
+ "prebuild": "yarn run tcm",
"build": "node scripts/build.js",
+ "pretest": "yarn run tcm",
"test": "node scripts/test.js --env=jsdom"
},

この状態で、 yarn start を実行し、 ‘typings’ 直下に ‘App.css.d.ts’ と ‘index.css.d.ts’ が生成されることを確認してください。(これは直前にセットで動く yarn prestart の ‘tcm’ による効果です。)

生成されるファイルがコミットされるのを防ぐため、 ‘.gitignore’ にその設定を加えます。

.gitignore
 yarn-error.log*

+# generated .css.d.ts files
+typings/**/*.css.d.ts

最後の設定変更として、 webpack の設定ファイルである ‘config/webpack.config.dev.js’ を変更します。(参考: How to Use CSS Modules with Create React App – Nulogy – Medium
通常の CSS Modules の設定に加え、 yarn start 中でも .css.d.ts ファイルが生成されるように “typed-css-modules-loader” の設定をします。
localIdentName は、ブラウザの開発者ツールでの見分けやすさのために設定しています。 sourceMap も同様に、開発時に元ファイルにジャンプできるように設定しています。)

config/webpack.config.dev.js
               require.resolve('style-loader'),
{
loader: require.resolve('css-loader'),
options: {
- importLoaders: 1
+ importLoaders: 2,
+ modules: true,
+ localIdentName: '[name]__[local]___[hash:base64:5]',
+ sourceMap: true
},
},
+ {
+ loader: require.resolve('typed-css-modules-loader'),
+ options: {
+ camelCase: true,
+ searchDir: './src',
+ outDir: './typings'
+ }
+ },
{
loader: require.resolve('postcss-loader'),

‘config/webpack.config.prod.js’ は、 webpack が動く前に “tcm” が動いていて、一実行では一度だけパイプラインを通る前提なので、 “typed-css-modules-loader” 自体を設定する必要はないように思います。

config/webpack.config.prot.js
                     {
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
+ modules: true,
minimize: true,
sourceMap: shouldUseSourceMap
},
},

この方法だけでは CSS への変更が反映されるのが 1 ステップ遅れてしまうのが残念ですが、この記事ではそれ以上の解決は行いません。
touch を行うような webpack ローダを挟むとうまく回りました。

さて、準備が整ったところで TypeScript のコードのほうに移りたいところですが、現状では ‘index.css.d.ts’ にエラーが出ているかと思います。
おそらくクラスセレクタで指定したスタイルが無い場合の出力結果が良くないようです。
そこで、すこし残念ですが、都合上偽のセレクタを追加して回避してしまいます。

src/index.css
 }
+
+.phony {
+}

ようやく ‘App.tsx’ を CSS Modules に対応するよう書き換えていきますが、その前に ‘App.css’ のセレクタを CSS Modules っぽく変えていきます。
(この記事では、後述するケースを出すために、あえて 2 単語のセレクタを例として導入しています。そのため、一部余分な変更点があります。 .intro.intro-area

src/App.css
-.App {
+.root {
text-align: center;


-.App-logo {
+.logo {
- animation: App-logo-spin infinite 20s linear;
+ animation: logo-spin infinite 20s linear;
height: 80px;


-.App-header {
+.header {
background-color: #222;


-.App-title {
+.title {
font-size: 1.5em;


-.App-intro {
+.intro-area {
font-size: large;


-@keyframes logo-spin {
+@keyframes App-logo-spin {
from {

ここで今度こそ ‘App.tsx’ を変更します。
基本的には、 className="foo-bar"className={style.fooBar} と書き換えるだけです。

こうすることで、補完が効くようになりますし、存在しないセレクタを記述しているとコンパイルエラーになってくれます。
エディタによってはもともと補完が効いていたかもしれませんが、こちらのほうが型システムに組み込まれているので、より安全です。(多分)

src/App.tsx
 import * as React from 'react';
-import './App.css';
+import * as style from './App.css';


return (
- <div className="App">
+ <div className={style.root}>
- <header className="App-header">
+ <header className={style.header}>
- <img src={logo} className="App-logo" alt="logo" />
+ <img src={logo} className={style.logo} alt="logo" />
- <h1 className="App-title">Welcome to React</h1>
+ <h1 className={style.title}>Welcome to React</h1>
</header>
- <p className="App-intro">
+ <p className={style.introArea}>
To get started, edit <code>src/App.tsx</code> and save to reload.

ここまでの修正を行うと、こんな感じになります。
yarn start で起動させたページ http://localhost:3000 をブラウザの開発者ツールを使って確認すると、 HTML タグのクラスが変化しているのがわかるかと思います。

Unit Test with CSS Modules and TypeScript

この章では、(主にコンポーネントの)単体テストのためのツールを設定してきます。

Jest

ここまででは実装そのものについての統合を行ってきましたが、ここでは単体テスト用のツールの統合をしていきます。
この記事では、 “create-react-app” にデフォルトで含まれている Jest を利用します。
AVA も興味があったのですが、テスト実行前に TypeScript の(明示的な)コンパイルが必要なため、そうでない Jest を記事内では使うことにしました。
React も Jest も Facebook 製なので、相性も良いことでしょう。

‘eject’ した後の状態でもテストケースは存在し、それは現状では正常にテストを実行できます。 yarn test で実行でき、選択すればウォッチモードで動かすことができます。

さて、このままではテストケースが粗すぎるため、もう少しテストケースを追加してみます。
よくある構成に則り今回は Enzyme を使ってテストをしていきますが、途中で Jest の設定を編集するため、 Jest の設定にもすこし触れておきます。
( Jest はテストのためにキャッシュを行います。もし設定を変更しても効果がない場合は、 Jest をバージョン 22 以上にした上で jest --clearCache を動かしてみてください。参考: jestjs - How to clear Jest cache? - Stack Overflow

ひとまず、現時点での Jest の設定は ‘package.json’ に書かれているため、それを別ファイル ‘jest.config.js’ として切り分けておきます。
それに加え、 ‘jest.config.js’ に対して TSLint が警告しないように ‘tsconfig.json’ の "exclude" にファイル名を加えておきます。

package.json
   },
- "jest": {...},
"babel": {
jest.config.js
module.exports = {...}
tsconfig.json
     "src/setupTests.ts",
- "config"
+ "config",
+ "jest.config.js"
]

この記事で編集する Jest のテスト設定で重要な項目として、 transformmoduleNameMapper があります。
大まかに感覚的に言えば、前者はファイルが読み込まれたときに行う動作、後者は読み込む対象にモックっぽい動作を行わせるための設定スクリプトファイルを指定できます。

Enzyme

さて、ここから Enzyme で実際に単体テストを記述していきます。(参考: Configuring React 16 + Jest + Enzyme + Typescript – Mateusz Sokola – Medium

まずは、必要なパッケージと型定義ファイルをプロジェクトに追加します。

$ yarn add -D enzyme @types/enzyme enzyme-adapter-react-16

続いて、 Enzyme でのテストでの共通処理を設定する必要があります。
ここでは、 ‘jest.config.js’ へオプションを追加し、対応するファイルを作成します。

jest.config.js
   collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'],
- setupFiles: ['<rootDir>/config/polyfills.js'],
+ setupFiles: [
+ '<rootDir>/config/polyfills.js',
+ '<rootDir>/config/jest/setupEnzyme.js'
+ ],
testMatch: [
src/config/jest/setupEnzyme.js
1
2
3
4
5
const enzyme = require('enzyme');
const Adapter = require('enzyme-adapter-react-16');

enzyme.configure({ adapter: new Adapter() });

また、テストファイル内で TSLint がテストファイルをチェックする際にも、開発用パッケージ devDependencies を使っているファイルに警告を出すので、それを抑えます。
この記事では ‘tsconfig.json’ でテストファイルを "exclude" する方法を採用します。
‘tslint.json’ で "no-implicit-dependencies" の設定をするのも手かもしれません。

tsconfig.json
     "config",
- "jest.config.js"
+ "jest.config.js",
+ "src/**/__tests__/**/*.ts?(x)",
+ "src/**/?(*.)(spec|test).ts?(x)"
]

これで共通の設定は終わりましたので、 Enzyme によるテストを記述してみます。
(エディタが行ってくれる警告を消すために、ここでも警告を抑えるコメントを加えています。)

src/App.test.tsx
+// tslint:disable-next-line:no-implicit-dependencies
+import * as enzyme from 'enzyme';
import * as React from 'react';
0
/* 中略 */
12
13
14
15
it('renders the root', () => {
const app = enzyme.shallow(<App />);
expect(app.find('.root').exists()).toBe(true);
});

ここの状態で yarn test を実行すると、どうなるでしょうか。
これは成功するように思えますが、実際には追加したテスト “renders the root” が失敗してしまいます。
デバッガを使って確認すると、どうやらテスト中には React コンポーネントの classNameundefined となってしまっているようです。

この原因は、先ほど設定した CSS Modules がまだ Jest や Enzyme でのテストに適応できていないためです。
さて、 Jest においては CSS ファイルはどのように扱われているのでしょうか。

先ほど transform という項目を取り上げました。ここで ‘jest.config.js’ を覗いてみると、そこにはこのような行があります。

jest.config.js
13
14
15
16
17
transform: {
'^.+\\.tsx?$': '<rootDir>/config/jest/typescriptTransform.js',
'^.+\\.css$': '<rootDir>/config/jest/cssTransform.js',
'^(?!.*\\.(js|jsx|mjs|css|json)$)': '<rootDir>/config/jest/fileTransform.js'
},

この ‘config/jest/cssTransform.js’ を覗いてみましょう。

config/jest/cssTransform.js
7
8
9
process() {
return 'module.exports = {};';
},

これを簡単に解釈すると、「 Jest でテストするときに CSS ファイルが読み込まれるとき、 module.exports = {}; という JavaScript コードとして読み込まれる。」になると思います。
つまり、 CSS Modules を使うように書き換えた className は、テスト時は空オブジェクトのプロパティを参照しており undefined になるということです。

この事態への最も簡単な解決法は、Using with webpack · Jest # Mocking CSS Modules にあるように、 identity-obj-proxy を使うことです。
これを設定すれば、 CSS Modules でよく使うような、一単語のセレクタばかりを定義している場合には対応できます。
この記事では、前述のように二単語のセレクタも(無理やり)含めたので、別の対策を考えます。

TypeScript からセレクタを使う場合は lowerCamelCase でアクセスしていると仮定し、 CSS のセレクタは kebab-case でアクセスしていると同じく仮定します。
これに合うように identity-obj-proxy を改造していけば、最低限必要なものができそうです。

まず、 ‘jest.config.js’ の transform から CSS に関する部分を削除し、 moduleNameMapper に新たな設定スクリプトファイルの指定を加えます。

jest.config.js
   transform: {
'^.+\\.tsx?$': '<rootDir>/config/jest/typescriptTransform.js',
- '^.+\\.css$': '<rootDir>/config/jest/cssTransform.js',
'^(?!.*\\.(js|jsx|mjs|css|json)$)': '<rootDir>/config/jest/fileTransform.js'
},

moduleNameMapper: {
- '^react-native$': 'react-native-web'
+ '^react-native$': 'react-native-web',
+ '^.+\\.css$': '<rootDir>/config/jest/cssMapper.js'
},

‘config/jest/cssTransform.js’ は削除し、新たに ‘config/jest/cssMapper.js’ を作成して、以下のように記述します。(参考: identity-obj-proxy, Convert a string from camel case to kebab case.

config/jest/cssMapper.js
1
2
3
4
5
6
7
8
9
10
11
12
module.exports = new Proxy(
{},
{
get: function getter(target, key) {
if (key === '__esModule') {
return false;
}
// lowerCamelCase to kebab-case
return key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
}
);

この状態で yarn test を実行すると、成功するようになっていると思います。
もし失敗しているのであれば、前述の jestjs - How to clear Jest cache? - Stack Overflow に従ってキャッシュのクリアを行ってみてください。

ここまでの変更を行い、他にも ‘App.test.tsx’ に単体テストを加えるとこんな感じになります。

UI Component Development with TypeScript

この章は、コンポーネント開発に関わるツールを統合してきます。
この記事では、開発とドキュメント化の両方を助けてくれるツールを取り挙げます。

Storybook

まずは Storybook を使います。
これを使うと変化を見ながら編集できるため、開発が捗るとともに、スナップショットの作成機能により、表示が意図せず変化した場合でも検知が可能です。

それでは、実際に導入していきます。(参考: typescriptでReact Storybookを試す。 - Qiita
プロジェクトのディレクトリの中で以下のコマンドを実行してください。

$ yarn global add @storybook/cli
$ getstorybook

最低限の処理はこれだけです。 yarn run storybook を実行するとサーバーが立ち上がり、表示される URL http://localhost:6006 にアクセスするとストーリーが表示されます。
この状態ではこんな感じになっています。

この状態では TypeScript によるストーリーは記述できませんので、対応を行っていきます。
同時に、ディレクトリの使い方もコンポーネント毎にまとまるように変えていきます。

最初に、ストーリーが記述されているファイルを探す設定を編集します。

.storybook/config.js
-// automatically import all files ending in *.stories.js
-const req = require.context('../stories', true, /.stories.js$/);
+// automatically import all files ending in *.stories.tsx or jsx
+const req = require.context('../src', true, /\.stories\.[jt]sx?$/);
function loadStories() {

‘stories/index.stories.js’ を ‘src/index.stories.js’ に移動し、正しく動くことが確認できたら、拡張子が ‘tsx’ のストーリーに対しても動くように設定を変えていきます。
まずは不足している型定義ファイルを追加します。

$ yarn add -D @types/storybook__react @types/storybook__addon-actions

そして、例として SpecialButton コンポーネントを作成しました。

src/components/SpecialButton/SpecialButton.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
import * as React from 'react';

import * as style from './SpecialButton.css';

export default (props: {
children?: string;
onClick(event: React.MouseEvent<HTMLButtonElement>): void;
}) => (
<button className={style.root} onClick={props.onClick}>
<span className={style.content}>{props.children}</span>
<span className={style.description}> is awesome.</span>
</button>
);
src/components/SpecialButton/SpecialButton.css
1
2
3
4
5
6
7
8
9
10
11
12
.root {
border: 1px solid red;
background-color: aliceblue;
}

.content {
font-size: large;
}

.description {
color: darkturquoise;
}

これに対するストーリーを記述します。(単体テストは後回し。)

scr/components/SpecialButton/SpecialButton.stories.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
// tslint:disable:no-implicit-dependencies
import { action } from '@storybook/addon-actions';
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import SpecialButton from './SpecialButton';

storiesOf('SpecialButton', module)
.add('with text', () => (
<SpecialButton onClick={action('clicked')}>Hello</SpecialButton>
))
.add('with some emoji', () => (
<SpecialButton onClick={action('clicked')}>😀 😎 👍 💯</SpecialButton>
));

さて、この状態で yarn run storybook を実行すると、対応するローダーを指定しろというエラーが出てきます。
デフォルトでは TypeScript のための設定はされていませんので、 ‘.storybook/webpack.config.js’ を編集して対応します。

.storybook/webpack.config.js
+const dev = require('../config/webpack.config.dev');
+
module.exports = {
plugins: [
// your custom plugins
],
- module: {
- rules: [
- // add your custom rules.
- ],
- },
+ module: dev.module,
+ resolve: dev.resolve
};
config/webpack.config.dev.js
             // by webpacks internal loaders.
- exclude: [/\.js$/, /\.html$/, /\.json$/],
+ exclude: [/\.js$/, /\.html$/, /\.json$/, /\.ejs$/],
loader: require.resolve('file-loader'),

上は、今まで設定してきた TypeScript や CSS Modules の設定をそのままコピーしてきています。これはかなりズボラな解決方法ですが、動きます。
下は、 .ejs ファイルを “file-loader” では読み込まないようにする設定です。
Storybook のページは .ejs ファイルによって作成されているようですが、今までの設定では .ejs のことを考慮していないので修正が必要になりました。(参考: Uncaught Error: Target container is not a DOM element. · Issue #2615 · storybooks/storybook

加えて、現状では yarn run storybook をしても CSS の .d.ts は生成されるようにはなっていないので、 ‘package.json’ にはフックを追加します。

package.json
     "test": "node scripts/test.js --env=jsdom",
+ "prestorybook": "yarn run tcm",
"storybook": "start-storybook -p 6006",
+ "prebuild-storybook": "yarn run tcm",
"build-storybook": "build-storybook"

最後に、 yarn run build-storybook を実行する際に生成されるファイルを git の管理と TypeScript の検査対象から除外するために、 ‘.gitignore’ と ‘tsconfig.json’ を修正します。

.gitignore
 typings/**/*.css.d.ts

+# generated storybook static files
+storybook-static
tsconfig.json
     "src/**/__tests__/**/*.ts?(x)",
- "src/**/?(*.)(spec|test).ts?(x)"
+ "src/**/?(*.)(spec|test).ts?(x)",
+ "storybook-static"
]

ここまでの設定に加え、さらにコンポーネントのテストも追加するとこんな感じになります。
yarn run storybook を実行し、 Storybook http://localhost:6006 に表示されるコンポーネントはこんな感じに表示されます。

SpecialButton > with text SpecialButton > with text
SpecialButton > with some emoji SpecialButton > with some emoji

StoryShots

Storybook に関連して、 StoryShots というアドオンがあります。
これは、ストーリーから Jest 用のスナップショットテストファイルを生成してくれるものです。

まず、必要なパッケージを追加します。

$ yarn add -D @storybook/addon-storyshots react-test-renderer @types/react-test-renderer

続いて、 StoryShots がスナップショットを作成・参照するためのテストを ‘src/storyshots.test.ts’ として追加します。
(残念ながら型定義ファイルがありませんので、コメントで抑えます。)

src/storyshots.test.ts
1
2
3
4
5
// @ts-ignore
// tslint:disable-next-line:no-implicit-dependencies
import initStoryshots from '@storybook/addon-storyshots';

initStoryshots();

さて、この状態で yarn test を実行すると、サンプルとして付属していた ‘src/index.stories.js’ の解釈に失敗してテストが失敗します。
Unexpected Token Import for ES6 modules · Issue #2081 · facebook/jest で解決できるようですが、この記事では( TypeScript メインなため)このファイルを削除して回避することにします。

この後にテストを再度実行すると、おそらくテストに一応成功し、 ‘src/__snapshots__/storyshots.test.ts.snap’ が生成されると思います。
(すぐに元に戻しますが、) ‘src/components/SpecialButton/SpecialButton.tsx’ での is awesome. is awesome! に変えて再度実行してみると、スナップショットと合わないというエラーが出るようになります。
普段は厄介に感じるかもしれませんが、意図せぬ描画の変更が発生した場合でもすぐに発見できるのがうれしいところです。

ここまでの変更で、こんな感じになります。

React Styleguidist

これで UI コンポーネントの開発やデモをやりやすくするツールは統合されましたが、使用法のインタラクティブなドキュメント作成のツールも存在するので、設定を加えてみます。
React Styleguidist は、コードの中のコメントからパラメータリストを生成したり、 Markdown で記述されたコードを動くサンプルとして表示したりします。

Configuring webpack — React Styleguidist - Create React App, TypeScript を参考にしつつ、設定を進めてきます。
まずは必要なパッケージのインストールを行います。

$ yarn add -D react-styleguidist react-docgen-typescript 

続いて、設定ファイルである ‘styleguide.config.js’ を作成します。コンポーネント本体でないものを無視する設定も忘れずに。
加えて、そのファイルを ‘tsconfig.json’ の設定から除外します。

styleguide.config.js
1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
components: 'src/components/**/*.{ts,tsx}',
propsParser: require('react-docgen-typescript').parse,
webpackConfig: require('./config/webpack.config.dev.js'),
ignore: [
'**/__tests__/**',
'**/*.test.{js,jsx,ts,tsx}',
'**/*.spec.{js,jsx,ts,tsx}',
'**/*.stories.{js,jsx,ts,tsx}',
'**/*.d.ts',
]
};
styleguide.config.js
     "src/**/?(*.)(spec|test).ts?(x)",
- "storybook-static"
+ "storybook-static",
+ "styleguide.config.js"
]

最後に、 ‘package.json’ に Styleguidist を起動するタスクを追加して準備完了です。

package.json
     "prebuild-storybook": "yarn run tcm",
- "build-storybook": "build-storybook",
+ "build-storybook": "build-storybook",
+ "prestyleguide": "yarn run tcm",
+ "styleguide": "styleguidist server",
+ "prestyleguide:build": "yarn run tcm",
+ "styleguide:build": "styleguidist build"
},

ここで yarn run styleguide を実行すると、 http://localhost:6060 に “SpecialButton” と表示された、何も情報がないページが開きます。
Styleguidist のページにしっかりしたドキュメントを表示させるには、ドキュメントコメントと Markdown ファイルが必要となります。

まずはドキュメントコメントを書いてみましょう。
この記事では設定しませんが、 TSLint の completed-docs を設定すると、ドキュメントコメントの記述を強制できます。
通常はコードにはドキュメントコメントをつけると思いますので、自然に公開用のコンポーネントの美しいドキュメントが生成されるという体験ができるはずです。

src/components/SpecialButton/SpecialButton.tsx
 
+/**
+ * Very simple but special button for the example.
+ */
export default (props: {
+ /**
+ * Content in this button.
+ */
children?: string;
+ /**
+ * Event handler when the user clicks this button.
+ * @param event Event argument for `onClick`
+ */
onClick(event: React.MouseEvent<HTMLButtonElement>): void;

次に、 Markdown でのドキュメントも加えてみましょう。
ページの説明にもあるように、同じディレクトリにある “readme.md” か、(拡張子を除いて)同名の Markdown ファイルを読み込んでくれるようです。
こちらも簡単に書いてみます。

src/components/SpecialButton/SpecialButton.md
1
2
3
4
5
6
7
8
9
10
11
## Simple

```jsx
<SpecialButton onclick={() => {}}>Simple</SpecialButton>
```

## with Emoji:

```jsx
<SpecialButton onclick={() => {}}>😘😎</SpecialButton>
```

新しく Markdown ファイルを追加した場合は、それを読み込ませるのには yarn run styleguide の再起動が必要です。
その後にページ http://localhost:6060 を開くと、このように表示されます。 “PROPS & METHODS” や “VIEW CODE” はクリックで開けます。
(コードはウェブページ上からも一時的に編集可能です。)

表示例 表示例

最後に、 yarn run styleguide:build を実行する際に生成されるドキュメントファイル群を git の管理と TypeScript の検査対象から除外するために、‘.gitignore’ と ‘tsconfig.json’ を修正します。

.gitignore
 storybook-static
+
+# generated styleguidist static files
+styleguide
tsconfig.json
     "storybook-static",
- "styleguide.config.js"
+ "styleguide.config.js",
+ "styleguide"
]

ここまでの設定で、こんな感じになります。

Snapguidist

Storybook のときと同じく、 Styleguidist に関連して styleguidist/snapguidist: Snapshot testing for React Styleguidist というツールがあります。
現状では Jest のスナップショットを生成するのは難しいようです。必要がなければ入れる必要はない物かもしれません。

Getting Started の節を参考にしつつ、導入を進めていきます。
まずは必要なパッケージをインストールします。

$ yarn add -D snapguidist

続いて、 ‘styleguide.config.js’ の中身を snapguidist でラップしてしまいます。

styleguide.config.js
-module.exports = {
+const snapguidist = require('snapguidist');
+
+module.exports = snapguidist({
components: 'src/components/**/*.{ts,tsx}',

]
-};
+});

設定はこれだけです。もう一度 yarn run styleguide を実行すると、 http://localhost:6060 で動作例の下に緑色のラインが出るようになります。
すぐ下のコードを編集するとラインが赤くなり、変化した場所が強調表示されます。どう変化するのか確かめるのには便利でしょう。
‘.snapguidist’ ディレクトリを覗いてみると、 ‘__snapshots__’ が生成されているのがわかります。

個人的には、Storybook 主体でコンポーネント開発を行い、そのドキュメントを Styleguidist で生成するのが良いのではないかと思います。

ここまでの設定を加えるとこんな感じになります。

End to End Test

この章では、 E2E テストを書くためのツールを設定してきます。
この記事では、(筆者が元々使ったことがあった) Puppeteer のみを扱っています。
他に面白そうなツールとして TestCafe があります。

Puppeteer

Puppeteer は、 Google Chrome (実際には Chromium ) をヘッドレスモードで動かすための Node ライブラリです。
テスト専用のライブラリというわけではなく、スクレイピングしたり、 HTML 文書を PDF 化したりすることもできるようです。
Chrome でしかチェックできないのが残念ですが、クロスブラウザを考慮する場合は TestCafe を検討するのもいいかもしれません。(未検証)

テストフレームワークとしてはすでに Jest を使えるようになっている状態なので、基本的には Using with puppeteer · Jest を参考に設定していけばいいはずです。

まずは、必要なパッケージをインストールします。また、 eject されたものよりも新しいバージョンの Jest を使わないとうまく動きませんので、 Jest のバージョンアップも行います。
Chromium を含めた本体もインストールするため、長いです。
最後の “serve” はグローバルのものを使うようにするのもいいと思います。

$ yarn add -D puppeteer @types/puppeteer jest-puppeteer serve
$ yarn upgrade jest ts-jest --latest

続いて、これを Jest の設定ファイルに取り込んでいきます。
この記事を書き始めた頃は xfumihiro/jest-puppeteer-example: A working example of jest with puppeteer を参考にしつつ、すべて自分で設定しなければなりませんでしたが、今では上記のパッケージを使うようにするだけで済みます。
上書きするなどしないよう、単体テスト用の Jest 設定ファイルとは別に、 ‘jest.e2e.config.js’ を作成します。
ここでは、 E2E テストは ‘e2e’ ディレクトリ以下のファイルに書くこととします。

jest.e2e.config.js
1
2
3
4
5
6
7
8
9
10
11
module.exports = {
preset: 'jest-puppeteer',
testMatch: ['<rootDir>/e2e/**/*.ts?(x)'],
transform: {
'^.+\\.tsx?$': '<rootDir>/config/jest/typescriptTransform.js',
},
moduleFileExtensions: ['js', 'jsx', 'json', 'ts', 'tsx'],
globals: {
'host': 'localhost:5001'
}
};

preset: 'jest-puppeteer' だけで、 Puppeteer 本体を使うための設定が完了します。
今回はローカルに立てるサーバーでビルド生成物をテストしますので、その設定も ‘jest-puppeteer.config.js’ として新しく作成します。
(これはローカルにインストールした “serve” を使う設定です。また、デフォルト設定で立ち上げたものと競合しないよう、ポートも変更しています。)

jest-puppeteer.config.js
1
2
3
4
5
6
module.exports = {
server: {
command: 'yarn serve -s build -p 5001',
port: 5001
}
};

実際のテスト実行の前に、少しだけ ‘App.tsx’ を編集してみます。
コンポーネントとして作成したボタンを配置し、クリックすると alert を表示するようにします。
(スナップショットが変化するので、あとで更新が必要になります。)

src/App.tsx
         <p className={style.introArea}>
- To get started, edit <code>src/App.tsx</code> and save to reload.
+ To get started, edit <code>src/App.tsx</code> and save to reload.<br />
+ <SpecialButton onClick={() => alert('Alert!')}>React</SpecialButton>
</p>

それでは実際のテストコードを記述します。
‘e2e/general.ts’ を以下のように書きました。

e2e/general.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// tslint:disable-next-line:no-implicit-dependencies
import { Page } from 'puppeteer';

declare const page: Page;
declare const host: string;

describe('App', () => {
beforeAll(async () => {
await page.goto(`http://${host}/`);
});

it('should display "Welcome to React" text on page', async () => {
const text = await page.evaluate(() => document.body.textContent);
expect(text).toContain('Welcome to React');
});

it('should alert when clicks "React" button', async () => {
let showsAlert = false;
page.on('dialog', async dialog => {
await dialog.dismiss();
expect(dialog.type()).toBe('alert');
showsAlert = true;
});
await page.click('button');
expect(showsAlert).toBe(true);
});
});

pagebrowser 等の変数は、ツールがグローバル変数として用意してくれます。
Provide TypeScript typings? · Issue #8 · smooth-code/jest-puppeteer を読む限り、執筆時点では TypeScript でも書きやすくなるには少し時間がかかりそうに思います。
host は ‘jest.e2e.config.js’ の globals に書いたため、利用できます。)
このテストでは、ビルド生成物が、 “Welcome to React” を表示するか、ボタンを押すと alert が表示されるかをテストします。

テストを実行する前に、 ‘package.json’ にタスクを追加します。 --config オプションを指定するのがミソです。

package.json
     "prestyleguide:build": "yarn run tcm",
- "styleguide:build": "styleguidist build",
+ "styleguide:build": "styleguidist build",
+ "pretest-e2e": "yarn build",
+ "test-e2e": "jest --config=jest.e2e.config.js"
},

また、設定ファイルへのリントエラーも再発するため、 ‘tsconfig.json’ を再び編集して除外するようにします。

tsconfig.json
     "styleguide.config.js",
- "styleguide"
+ "styleguide",
+ "jest.e2e.config.js",
+ "jest-puppeteer.config.js"
]

ここで yarn run test-e2e を実行すると、見えないところで Chromium が立ち上がってテストが実行されます。
pretest-e2e を設定したことにより yarn build も実行されるようにしているため、開始まで少し時間がかかります。手っ取り早く行いたければ yarn jest --config=jest.e2e.config.js を実行するのが良いでしょうか。)

最後に、 yarn test --updateSnapshot を実行して古くなったスナップショットを削除します。(今回は本来の原因とはちょっと違いますが…)
ここまででこんな感じになりました。

落ち穂拾い・展開

ここまで諸々のツールを挙げてきましたが、 React の関連ツールは大量にありますし、フロントエンドのツールまで広げると星の数ほどあります。
ここにあるのはその中の極一部のツールや概念です。関連ライブラリの関連ライブラリ等含めると、キリがありません。

React 関連

  • react-router
    ルーティング
  • Redux, MobX
    状態管理
  • i18n (internationalization)
    多言語対応
  • Server Side Rendering, Universal Rendering
    初回読込時間と SEO 対策
  • Preact
    軽量版 React

Lint ツール

  • markdownlint
    For Markdown ( Node.js 版)
    Styleguidist と組み合わせると良さそう。
  • stylelint
    For CSS
  • JSON Lint
    For JSON
  • ESLint
    For JavaScript (設定ファイルに対して)

テスト関連

立ち上がるサーバーの一覧

すべて設定を編集していない場合のものです。

  • yarn run: http://localhost:3000
  • serve: http://localhost:5000
  • Storybook: http://localhost:6006
  • Styleguidist: http://localhost:6060

その他雑多

  • Sass, Stylus
    webpack 用の各ローダーについては、 “css-loader” の前までに実行するように設定する。
  • yarn start でブラウザが勝手に立ち上がるのをやめさせる。
    -> ‘scripts/start.js’ で openBrowser のある行を削除
  • The 100% correct way to structure a React app (or why there’s no such thing)
    コンポーネントのディレクトリ構成戦略
    Webpack の設定を上手くすることで実現可能