React + TypeScriptで作ったWebシステムの一年後の話


この記事は「Opt Technologies Advent Calendar 2017」１日目です。
[２日目の記事](https://qiita.com/mshibuya/items/2716e3a12618c2f54cbf)のほうが先に出ている気がしますが気のせいでしょう。

## はじめに

去年のアドベントカレンダーにて、[「React + TypeScriptでWebシステムを作った話」](https://qiita.com/uryyyyyyy/items/b326ccca58446d1e853e)を書きました。

本記事では、上記システムで使っている技術が一年経ってどう変わったかを書こうと思います。

（ちなみに筆者は、上記システムにおいては開発はしておらず、フロントエンド中心に開発環境やライブラリ周りの整備だけしています。）
（社内的な要件や技術スタックは、上記記事に書いているのでぜひご覧ください。）

## package.jsonの抜粋

去年のはこのような形ですね。 [package.jsonの抜粋](https://qiita.com/uryyyyyyy/items/b326ccca58446d1e853e#packagejson%E3%81%AE%E6%8A%9C%E7%B2%8B)

今の状態はこんな感じです。

```json:package.json
{
  "name": "~~~",
  "version": "1.0.0",
  "scripts": {
    "postinstall": "rm -f ./node_modules/immutable/dist/*.d.ts && sh ./moveToPublic.sh",
    "build:watch": "webpack --progress --colors --watch --config ./webpack.config.dev.js",
    "build": "webpack --colors --config ./webpack.config.dev.js",
    "build:prod": "webpack --colors --config ./webpack.config.prod.js",
    "test:ut": "karma start karma.conf.js",
    "test:all": "karma start karma.conf.js **/*.spec.tsx **/*.spec.ts",
    "test:coverage": "tsc && karma start karma.conf.js ./dist/**/*.spec.js",
    "server": "node dev-server.js",
    "lint:unit": "tslint -c tslint.json",
    "lint:ci": "git diff --name-only --no-renames HEAD $(git merge-base HEAD origin/master) | grep ^web-console/front/src | sed -e 's@web-console/front/@@' | xargs npm run lint:unit dummy",
    "lint:unit-fix": "tslint --fix -c tslint.json",
    etc...
  },
  "devDependencies": {
    "@types/~~~",
    "enzyme": "3.2.0",
    "enzyme-adapter-react-16": "1.1.0",
    "express": "4.16.2",
    "fetch-mock": "5.13.1",
    "jasmine-core": "2.8.0",
    "karma": "1.7.1",
    "karma-chrome-launcher": "2.2.0",
    "karma-jasmine": "1.1.0",
    "karma-mocha-reporter": "2.2.5",
    "karma-webpack": "2.0.6",
    "multer": "1.3.0",
    "react-test-renderer": "16.1.1",
    "ts-loader": "3.1.1",
    "tslint": "5.8.0",
    "tslint-config-standard": "7.0.0",
    "tslint-react": "3.2.0",
    "typescript": "2.6.1",
    "uglifyjs-webpack-plugin": "1.1.0",
    "webpack": "3.8.1",
    etc...
  },
  "dependencies": {
    "classnames": "2.2.5",
    "date-fns": "1.29.0",
    "immutable": "3.8.2",
    "jquery": "3.2.1",
    "materialize-css": "0.98.0",
    "react": "16.1.1",
    "react-dom": "16.1.1",
    "react-materialize": "1.1.0",
    "react-redux": "5.0.6",
    "react-router-dom": "4.2.2",
    "react-select": "1.0.0-rc.10",
    "redux": "3.7.2",
    etc...
  }
}
```

順番にお話していこうと思います。

## 画面デザインについて

変わらず[materialize](http://materializecss.com/) ベースになっています。
一部ややこしいコンポーネントは[react-matrialize](https://github.com/react-materialize/react-materialize)というライブラリを使っていて、問題なく運用出来てたり、たまにPR出してみたりしています。
このライブラリが最近React v16対応するまでは `npm install` したあとにreact v15のライブラリを削除してビルドする、という邪悪なことをしていました。。（もちろんテスト通ることは確認した上で）

## Reactのバージョンアップ

Reactをv16にバージョンアップしました。上記システムにおいて、reactを上げる際に対応したのは以下の点です。

- 削除されるAPIへの依存の撤廃
- min.jsの配置が変わった
- テスト時にライフサイクル系のメソッドが一部呼ばれなくなった

### 削除されるAPIへの依存の撤廃

react v15.5で、削除されるAPIを利用している場合にwarningが出るようになりました。
https://reactjs.org/blog/2017/04/07/react-v15.5.0.html#new-deprecation-warnings

自分たちで書いているコードは問題ないのですが、利用しているライブラリで問題があったので、issueのやり取りを追いながらちまちま対応していました。

### min.jsの配置が変わった

上記システムでは、ビルド時間短縮のためreactなどのコードはバンドルせずにmin.jsをそのまま配置して読ませるようにしています。
v15系では `react/dist/react.min.js` にあったのですが、 `react/umd/react.production.min.js` に置かれるようになったのでそこだけ対応しました。

### テスト時にライフサイクル系のメソッドが一部呼ばれなくなった

React v16に上げた際に、enzymeを使った単体テストで `componentDidMount` などのメソッドが呼ばれなくなっていました。
これはenzymeのバージョンを上げて、 [enzyme-adapter-react-16](https://github.com/airbnb/enzyme/tree/master/packages/enzyme-adapter-react-16) を使うことで解消出来ました。

## TypeScriptのバージョンアップ

こちらは特にバージョンを上げるにあたって苦労した記憶はないのですが、型の表現力が増してきてredux周りのコードがよりキレイに書けるようになってきました。

詳しくは筆者の別記事「[React + Redux + TypeScriptの最小構成](https://qiita.com/uryyyyyyy/items/3ad88cf9ca9393335f8c)」に書いてありますが、こんな感じです。

```ts:module.ts
import {Action} from 'redux'

enum ActionNames {
  INC = 'counter/increment',
  DEC = 'counter/decrement',
}

interface IncrementAction extends Action {
  type: ActionNames.INC
  plusAmount: number
}
export const incrementAmount = (amount: number): IncrementAction => ({
  type: ActionNames.INC,
  plusAmount: amount
})

interface DecrementAction extends Action {
  type: ActionNames.DEC
  minusAmount: number
}

export const decrementAmount = (amount: number): DecrementAction => ({
  type: ActionNames.DEC,
  minusAmount: amount
})

export type CounterActions = IncrementAction | DecrementAction

export default function reducer(state: CounterState = initialState, action: CounterActions): CounterState {
  switch (action.type) {
    case ActionNames.INC:
      return {num: state.num + action.plusAmount}
    case ActionNames.DEC:
      return {num: state.num - action.minusAmount}
    default:
      return state
  }
}
```

- enumの値にstring型を使えること
- literal型が使えること
- switch句の分岐先で型がより厳密になること

などを利用しています。

（ただ、下記の実装は[TypeScript2.6](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-6.html)で入ったstrictFunctionTypesを使ってしまうとエラーで弾かれてしまいます。
これは、reducerの引数のactionは本来 `Action`型のどんなオブジェクトでも入りうるのだけど `CounterActions` という型だけを想定してしまっているからです。型に厳密にやろうとしてもどこかでキャストせざるを得ず、reduxの限界を感じています。。。）

## react-router v4対応

react-routerの使い方や気にすべき点に関しては別記事「[React + Redux + TypeScriptでreact-router](https://qiita.com/uryyyyyyy/items/30733e9cd140e60c52bd)」にてまとめているのですが、route設定やテストなどかなり差分が出て大変でした。
特にページ遷移時のトリガーの変更については、かなり汚いハックをしてしまいました。。

```ts
const history = createBrowserHistory()
let wasChanged = false;

//ページ内で何か編集中だと wasChanged がtrueになる

window.addEventListener('beforeunload', (e: any) => {
  if(wasChanged) e.returnValue = ""; // Chromeではメッセージを選べないらしい
});

history.block(() => {
  if (wasChanged)
    return '編集中ですが画面遷移しても良いですか？'
})

//ページ遷移したあとは再びfalseに戻す
history.listen(() => {
  wasChanged = false;
})
```


## lintツールの導入

プロジェクト初期では必要としなかったのですが、筆者が開発から退くこともあり、他メンバーの要望もあり、lintチェックを入れようということになりました。
しかし既存のコードが大量にある状態からlintを導入すると、とても直せる量じゃないエラーが出てきてしまいます。
チェックを緩くして徐々に、という手もあるのですが、それだとlintの意味が薄くなると思い、「編集したコードについてだけ厳しいlintチェックを掛ける」という運用にしました。
npm scriptの `lint:ci` というのがそれです。masterブランチと比較して差分のあるファイルだけを対象にしています。

## まとめ

上記システムのフロントエンドの技術スタックですが、一年経っても負債化することなく順調に進化できています。
筆者が開発から退いてしばらく経ちますが、開発においても問題なく回っているようです。

今後も、メンテナンス性も考慮しつつ新技術を積極的に取り入れていきたいと思います。