初めてのReactで初めてのSPAを作りました

全てが初めてでもなんとかリリースできたので、同じくこれからReactなりSPAなりに挑戦しようと思っている方に、何か参考になれば嬉しいです。

## スケジュール感

2018年3月初 〜 2018年5月末

## フロント開発体制

１人

## スキル感

- `React`: チュートリアルを数年前に１度やったことがある
- `SPA`: API叩いて画面の一部を更新するくらいはやったことはあるものの、routerとか使ったことはない
- `js`: それなりにかけるかなーといった感じ

## 作ったもの

全10ページ程度のSPA
（まだ前面に押し出しているサービスではないので紹介ができません・・・）

## コーディング前にやったこと

### 1. 技術選定

もともとReactを使っていこうという雰囲気が社内にあり、Reactを使うことは決まっていました。
~~本当はvueがやりたかったんですが社内ではreact一択の雰囲気だったので~~

あとは個人的に使ってみたかったもの、話題になっていて使いやすそうなものを選んでいきました。

また、開発期間が短く、サーバーサイドのAPIが出来上がるのを待っていると明らかに間に合わなかったため、フロント主導のAPI設計のために`swagger`を使いました。

依存モジュールはこんな感じです。
※主要なモジュールについては後述します

```js
  "dependencies": {
    "ajv": "^6.1.1",
    "axios": "^0.17.1",
    "idx": "^2.3.0",
    "lodash": "^4.17.10",
    "react": "^16.2.0",
    "react-dom": "^16.2.0",
    "react-redux": "^5.0.6",
    "react-router-dom": "^4.2.2",
    "redux": "^3.7.2",
    "redux-saga": "^0.16.0",
    "styled-components": "^3.1.6"
  },
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-preset-env": "^1.6.1",
    "babel-preset-react-app": "^3.1.1",
    "body-parser": "^1.18.2",
    "browser-sync": "^2.23.6",
    "cpx": "^1.5.0",
    "cross-env": "^5.1.1",
    "express": "^4.12.3",
    "gulp": "^3.9.1",
    "gulp-autoprefixer": "^4.1.0",
    "gulp-iconfont": "^9.1.0",
    "gulp-iconfont-css": "^2.1.0",
    "gulp-imagemin": "^4.1.0",
    "gulp-load-plugins": "^1.5.0",
    "gulp-plumber": "^1.2.0",
    "gulp-rename": "^1.2.2",
    "gulp-sourcemaps": "^2.6.4",
    "gulp-stylus": "^2.7.0",
    "husky": "^0.14.3",
    "lint-staged": "^6.1.1",
    "minimist": "^1.2.0",
    "mkdirp": "^0.5.1",
    "nodemon": "^1.14.12",
    "npm-run-all": "^4.1.2",
    "opener": "^1.4.3",
    "prettier": "^1.10.2",
    "replace": "^0.3.0",
    "rimraf": "^2.6.2",
    "serve-favicon": "^2.5.0",
    "stylus": "^0.54.5",
    "swagger-express-mw": "^0.1.0",
    "touch": "^3.1.0",
    "wait-on": "^2.1.0",
    "watch": "^1.0.2",
    "webpack": "^3.11.0",
    "webpack-dev-server": "^2.11.1"
  },
```

### 2. 開発環境構築

- js: `webpack`
- static files: `gulp`
- タスク実行: `npm scripts`

js周りは全て`webpack`に、cssプリプロセッサや画像圧縮、その他静的ファイルの操作は`gulp`に、各ビルドの実行は`npm scripts`から行います。
`link-staged`、`prettier`を使いコードの整形は自動化しています。

`npm scripts`周りはこんな感じです。

```js
  "scripts": {
    "start": "node app.js",
    "prod": "cross-env NODE_ENV=production npm-run-all ready --parallel \"prod:* -- {@}\" --",
    "prod:imagemin": "gulp imagemin",
    "prod:replace": "gulp replace",
    "dev": "cross-env NODE_ENV=development npm-run-all ready --parallel dev:*",
    "dev:server": "nodemon app.js",
    "dev:client": "wait-on http://localhost:10010/docs/ && run-p dev:client:*",
    "dev:client:docs": "opener http://localhost:10010/docs/#/",
    "dev:client:components": "npm run components && opener http://localhost:10010/static/components.html",
    "dev:client:static": "npm run liveserver:static",
    "dev:watch": "npm run watch",
    "api": "run-p api:*",
    "api:server": "nodemon app.js",
    "api:client": "wait-on http://localhost:10010/docs/ && npm run liveserver:api",
    "components": "cross-env NODE_ENV=development webpack --config webpack.components.js",
    "components:dev": "cross-env NODE_ENV=development npm-run-all ready --parallel components:dev:*",
    "components:dev:server": "webpack-dev-server --config webpack.components.js",
    "build": "run-p build:*",
    "build:js": "webpack",
    "build:css": "gulp css",
    "build:assets": "cpx 'src/assets/**/*.*' public/static",
    "prebuild": "run-p _build:*",
    "_build:iconfont": "gulp iconfont",
    "watch": "run-p watch:*",
    "watch:js": "watch 'npm run build:js' src/js",
    "watch:css": "watch 'npm run build:css' src/css",
    "watch:assets": "watch 'npm run build:assets' src/assets",
    "clean": "rimraf public/static && mkdirp public/static && nodetouch public/static/.gitkeep",
    "ready": "run-s clean build",
    "liveserver:static": "browser-sync start -p http://localhost:10010/ -f public/static --startPath static/",
    "liveserver:api": "browser-sync start -p http://localhost:10010/ -f api/swagger/swagger.yaml --reload-delay 1000 --startPath docs/#/",
    "precommit": "lint-staged",
    "test": "echo 'write test'"
  },
  "lint-staged": {
    "*.js": [
      "prettier --write --single-quote 'src/js/**/*.js'",
      "git add"
    ]
  }
```

`npm scripts`周りでお世話になっている記事：
[npm-scripts で使える便利モジュールたち](https://qiita.com/mysticatea/items/12bb6579b9155fd74586)

### 3. モック作成

SPAを感覚を掴みたかったので、実際に作っていく画面から２、３画面を選択してモックを作りました。
この時に各モジュールのチュートリアルやドキュメントを読み込んでいます。

### 4. ドキュメント化

モックを作った時点である程度のドキュメント化はできそうだったので、社内で使っているコンフルエンスにドキュメントを残していきました。

ディレクトリ構造、処理の流れをフローチャートにしてみたりしました。

#### ディレクトリ構造

```
/root
  |- /api
    |- /{ダミーのレスポンスデータの設定ファイル}.js
  |- /config
    |- /{swaggerの設定ファイル}.yaml
  |- /public
    |- /{expressからアクセス可能な静的ファイル}.*
  |- /src
    |- /assets
      |- /{静的ファイル}.*
    |- /css
      |- /{resetなどの全画面共通CSS}.styl
    |- /js
      |- /actions
        |- /index.js
        |- /{index.jsから読み込まれるactionファイル}.js
      |- /components
        |- /atoms
          |- /{atom}.js
        |- /molecules
          |- /{molecule}.js
      |- /constants
        |- /{画面共通定数}.js
      |- /containers
        |- /{pages}
          |- /index.js
          |- /mapping.js
          |- /styled.js
      |- /helpers
        |- /{ヘルパー関数}.js
      |- /reducers
        |- /index.js
        |- /{index.jsから読み込まれるreducerファイル}.js
      |- /sagas
        |- /index.js
        |- /{index.jsから読み込まれるsagaファイル}.js
  |- その他設定ファイル（.babelrcとか）
```

基本は各モジュールのチュートリアルに沿ったディレクトリ構造です。

以下は独自に変更している点です。

- **atomic design**を採用したため、`conponents`配下に`atoms`と`molecules`を切った
- `container`が容易に膨れ上がることが見えていたため、cssを記述する`styled.js`とpropsへのマッピングをする`mapping.js`を切り出した

`mapping.js`は

``` javascript
import { connect } from 'react-redux';

const mapStateToProps = state => ({
  someProperty: value,
  someProperty: value
});

const mapDispatchToProps = dispatch => ({
  someProperty: value,
  someProperty: value
});

const mergeProps = (stateProps, dispatchProps, ownProps) => {
  // ...merge props
  return {
    mergedProperty: value,
    mergedProperty: value,
  };
};

export default function(Component) {
  Component = connect(mapStateToProps, mapDispatchToProps)(Component);
  return Component;
}
```

のようになっており、`index.js`で

``` javascript
// ...create ReactComponent
export default mapping(ReactComponent);
```

のように使っています。

#### フローチャート

フロント周りの処理フロー（以下画像）であったり、システム全体のフローなんかを作りました。

<img width="866" alt="chart.png" src="https://qiita-image-store.s3.amazonaws.com/0/100188/1b11287f-0e60-d700-5124-69c657a5e97d.png">


フローチャートの作成には[draw.io](https://www.draw.io/)を使いました。
google driveに保存できて便利です。

### 5. コーディング

ここまで作って、あとはひたすらコーディングをしていきました。

## 主要モジュール

今回使った主要なモジュールと、その時参考にした資料や気付きなんかを書いていきます。

### React

参考：[Tutorial: Intro To React](https://reactjs.org/tutorial/tutorial.html)

Reactのチュートリアルです。

### Redux

参考：[Basics](https://redux.js.org/basics)

Reduxのチュートリアル的なものです。
（私はいきなり[Example: Todo List](https://redux.js.org/basics/example-todo-list)を見てしまったのですが、Basicsからやれば順に細かく見て行けることに後から気付きました。。。）

### React Router DOM

参考：[React Router: Declarative Routing for React.js](https://reacttraining.com/react-router/web/guides/philosophy)

React Routerのドキュメントです。

画面遷移時のスクロール位置リセットは初めに組み込んでおくのが良いと思います。
[Scroll Restoration](https://reacttraining.com/react-router/web/guides/scroll-restoration)

### Redux Saga

参考：[redux-sagaで非同期処理と戦う from Qiita](https://qiita.com/kuy/items/716affc808ebb3e1e8ac)

Redux Sagaの中の人？が書いてくださったQiitaの記事です。
非同期処理を書く時に大変お世話になります。

非同期処理のactionをsagaで拾うようにして、非同期処理完了後にreducerに渡すようにします。

例：`REQUEST_HOGE`がdispatchされた場合

``` javascript
import { select, all, call, put, fork, takeEvery } from 'redux-saga/effects';
import * as actions from '../actions';
import { get } from 'axios';

export default function*() {
  yield takeEvery(actions.REQUEST_HOGE, fetchHoge);
}

function* fetchHoge() {
  let response = null;
  try {
    response = yield get('/api/hoge');
  } catch (err) {
    // error handring
    return;
  }

  yield put(
    type: actions.UPDATE_HOGE,
    response
  );
}
```

### Styled Components

参考：[Basics](https://www.styled-components.com/docs/basics)

Styled Componentsのドキュメントです。
Basicsだけでもやりたいことはほとんど出来るのではないかと思います。

他のモジュールと区別がつくようにしたくて、Styled Componetsのモジュールには`S_`のprefixを付けるようにしました。

#### Over 200 classes were generated for component Component.

Styled Componentsで生成されるクラスが200を超えるとwarningが出るようです。
ユーザー操作によるスタイルの変更などは`style`属性を使ったほうが良さそうです。

[Change warning message when creating too many classes](https://github.com/styled-components/styled-components/issues/1212)

### Swagger Express MW

参考：

- [swagger-express-mw](https://www.npmjs.com/package/swagger-express-mw)
- [Basic Structure](https://swagger.io/docs/specification/2-0/basic-structure/)

Swagger Express MWで使われているswaggerがv2.0のようで、v2.0の記法にリンクしています。

## Howto

こうしたよ、というお話です。

### ローディング

bundle.jsがとてつもなく大きくなるため、ローディングはjsの外に出すようにしました。

- index.html

``` html
<body>
<div id="loading">
  <!-- loading表示要素 -->
</div>
<div id="app"></div>
<script src="/static/bundle.js"></script>
</body>
```

js処理が終わったタイミング（LifeCycleのフックなど）で`Element.remove()`をするようにしています。

### thisの束縛

thisの束縛は`constructor`内で行うのが良いようです。

``` javascript
constructor(props) {
  super(props);

  this.handleFuga = this.props.handleFuga.bind(this);
}
```

参考：[React.jsのrenderの戻り値の中で.bindで新しい関数を定義してはいけないわけ](https://qiita.com/shuntksh/items/fd81ca9aa31ea8f962e2)

### 条件分岐による描画

好き嫌いが分かれそうですが、以下のような書き方もできるようです。

```jsx
render() {
  return (
    <div>
      {variable && (
        <p>piyo</p>
      )}
    </div>
  );
}
```

## 苦戦したこと

### フロントとサーバーの役割分担

データの整形など、フロントでもサーバーでもできるようなことは、なるべくサーバーに任せてしまうのが良いです。
フロントは受け取ったデータを描画するだけ。それ以外にもやることは多々ありますので、なんでも担ってしまうとすぐに破綻します。
これは事前にしっかり握っておくと良いと思います。

### 深すぎるstoreオブジェクト

ユーザーのデータを次々と取得していくとします。
大きな括りで見ればユーザーデータという１つのデータなので、１つのオブジェクトを拡張していくようにしました。

``` js
{
  user: {
    name: value,
    age: value,
    history: {
      school: [{
        ...someProperty
      }],
      business: [{
        ...someProperty
      }],
    },
  }
}
```

こうなるとreducerで更新するのがとても大変になります。
あまり深くなりすぎないように、できれば事前にstoreの構成は考えておくのが良いと思います。

### 抽象的なプロパティ名

`data`、`list`、`item`などの抽象的な単語をプロパティ名に使用して苦戦しました。
上の " 深すぎるstoreオブジェクト " もあり、ループで回すと悲惨なことになります。

``` javascript
list.forEach(item => {
  if (!item.list) return;
  item.list.forEach(/* 何を回しているか分からなくなる */);
});
```

面倒だったり多少気持ち悪かったとしても、なるべく限定的な単語を使用するのが良いと思います。

```
// danger
data
list
item

// safety
foodData
drinkList
dessertItem
```

### URL直叩き対応

SSRをしなかったため、URLを直叩きされた場合に「データがない！」ということが多々起きました。

トップから落ちてくるとデータが揃う、という構造になっていたため、コンポーネントのWillMountやsagasの中でデータの有無の判定し、なければ先に別のAPIを叩く、という対策をとりました。
" これからやりたいこと " でもあるHOCを使えば解決できるかもしれません。

### プロパティ有無の判定

プロパティ有無の判定が面倒で、全てtry-catchで囲み、エラーをキャッチしたらローディングコンポーネントを表示する、という対応をしていました。
当然参照エラー以外のエラーも発生するわけで、デバッグが非常にし辛くなりました。

`idx`か`optional-chaining`を使えればプロパティ有無の判定は楽になりますので、初めから導入しておくのが良いかと思います。

参照：

- [idx](https://github.com/facebookincubator/idx)
- [babel-plugin-proposal-optional-chaining](https://github.com/babel/babel/tree/master/packages/babel-plugin-proposal-optional-chaining)


## これからやりたいこと

全てが初めてだったために導入を諦めたもの、新たに知った（理解）したものがあるため、これから導入していこうと思います。

### Flow導入

静的型チェッカーです。

参照：[Flow](https://flow.org/)

### HOC導入

コンポーネントのラッパーで、コンポーネントを拡張します。
storeの初期化やローディングの出しわけを、HOCでモジュール化できればいいなと思っています。

参照：[ReactのHigher Order Components詳解 : 実装の2つのパターンと、親Componentとの比較](https://postd.cc/react-higher-order-components-in-depth/)

### storybook導入

コンポーネントの管理・動作確認

参照：[storybook](https://storybook.js.org/)

## おわりに

ほぼ全てが初めての取り組みでしたが、なんとかリリースすることができました。
もちろん作りの荒いところ、改善できるところは多々あります。。。

が、それでもリリースまでもっていけたのは純粋にReactやその周りのモジュールが超絶便利に作りこまれていること、知名度がありドキュメントが豊富なことだと思います。

JSXって・・・とか、俺は絶対vueだ！とか思ってましたが、reactとても良いです。便利です。

SPA開発に興味はあるけどまだ本格的に取り組んだことがない方、ぜひどこかに飛び込んでみてください。
なんとかなりますし、学びもすごく多いです。