ReactとMobXを使ったwebアプリの作り方

普段React+Reduxを用いてwebアプリを作っていて、[Front-End Developer Handbook 2017](https://www.gitbook.com/book/frontendmasters/front-end-handbook-2017/details)でMobXってワードが出ていて気になっていたのですが、日本語の記事が少なかったので調べてサンプル作ってみました。序盤はパッケージ選定とかの話しているので飛ばしてもらってもかまわないです。(雑な認識なので詳しい方いたら教えてください。。。)

Front-End Developer Handbook 2017の[このへん](https://frontendmasters.com/books/front-end-handbook/2017/learning/react-redux.html)

> When you have Redux mastered, take a look MobX or consider creating your own small custom Redux like implementation from scratch.

意訳

> 
Reduxをマスターしたら、MobXを見てみたり、独自のカスタムReduxをスクラッチで作ってみてください。


ルーティングとかは一旦入れず単純にReactからstateをMobXに管理してもらうだけのアプリを作成します。具体的にはよくあるカウンターアプリを実装します。(見栄えとかはクソ悪いです)

# 成果物

## demo

![](https://github.com/togana/sample-react-mobx/blob/master/demo/counter.gif?raw=true)

## コード

https://github.com/togana/sample-react-mobx

# 環境構築

react-create-appなどを使って雛形を生成してもいいのですが、中身を理解しづらくなるので自前で一つづつpackageを入れて構築していきます。

## package選定

### devDependencies周り

トランスパイルツールはwebpack+bableを使うことにします。
理由は単純ですが、Ruby on Rails等でも取り込まれてので、今後使用頻度が高くなると感じているためです。合わせてbableを使うことでトランスパイルします。

linterはeslintを使うことにしてます。
理由はかなりしっかりとlintしてくれる、自動fix機能があるためです。

#### babelについて

周辺ツールの利用用途を雑な認識でまとめておきます。
プリセットは下記を利用します。

- babel-preset-env
- babel-preset-react

bable-preset-envはサポートするブラウザを指定してそれに基づいて必要なプラグインとpolyfillを勝手に入れてくれるプリセットです。

もう一つのbabel-preset-reactはReact周りのプラグインのオール・イン版です。
jsx記法が使えるようにしたりするために入れています。

プラグインは下記を利用します。

- babel-plugin-transform-class-properties
- babel-plugin-transform-decorators-legacy
- babel-plugin-transform-runtime

babel-plugin-transform-class-propertiesはClassPropertiesを使用したいので導入しています。あとで記載するのですが、MobXのinjectを使うとPropTypesが暴れます。wrappedComponent.proptypesとかを利用すると解決するとREADMEに記載されているのでそちらを使うこともできます。

babel-plugin-transform-decorators-legacyは@を使いたいので導入しています。legacyとついていて気持ち悪いのですがbabel-plugin-transform-decoratorsではうまく変換できなかったためlegacyを使用しています。今後もしかしたら変更できるかもしれないです。

babel-plugin-transform-runtimeは古いブラウザ対応するためのpolyfillをいい感じに入れてくれます。envとかぶってる気もするのですが `regeneratorRuntime` のエラーが出てトランスパイルできないので入れています。こうした方がいいとかこうなるからとかあれば教えて貰いたいです。

上で書いたことを全部対応した設定ファイルが下記の `.babelrc` です。
3つ前のバージョンのブラウザかつSafari10以上をサポートするように設定しています。

```json
{
  "presets": [
    [
      "env",
      {
        "targets": {
          "node": "current",
          "browsers": [
            "last 3 versions",
            "safari >= 10"
          ],
          "modules": false,
          "loose": true
        }
      }
    ],
    "react"
  ],
  "plugins": [
    "transform-decorators-legacy",
    "transform-class-properties",
    "transform-runtime"
  ]
}
```

#### webpackについて

周辺ツールの利用用途を雑な認識でまとめておきます。

- webpack-dev-server
- html-webpack-plugin
- babel-loader

webpack-dev-serverは言わずですが動作確認をするためにwebサーバーを立ててくれます。基本webpackのconfigと同じものが使えるのですごく便利です。

html-webpack-pluginはテンプレートで用意しておいたhtmlを利用できます。トランスパイルしたjsファイルを動的にhtmlに差し込んだりできます。

babel-loaderはbabelと組み合わせて使うために導入しています。

上で書いたことを全部対応した設定ファイルが下記の `webpack.config.babel.js` です。

`webpack.config.babel.js` という名前にすると設定を読み込むときにbabelを通して変換して読み込んでくれるのでimport等が使えます。

```js
import path from 'path';
import webpack from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';

const NODE_ENV = process.env.NODE_ENV || 'development';
const outputPath = path.join(__dirname, 'dist');

export default {
  devtool: false,
  entry: {
    bundle: './src/index.jsx',
  },
  output: {
    path: outputPath,
    publicPath: '/',
    filename: '[name].js',
  },
  module: {
    rules: [
      { test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader' },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      hash: true,
      filename: 'index.html',
      template: './src/index.template.ejs',
      inject: 'body',
    }),
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify(`${NODE_ENV}`),
      },
    }),
  ],
  resolve: {
    extensions: ['.js', '.jsx'],
  },
};
```

#### eslintについて

周辺ツールの利用用途を雑な認識でまとめておきます。

- babel-eslint
- eslint-config-airbnb

babel-eslintはeslintのパーサーにbableを使用してくれます。

eslint-config-airbnbはeslintの設定をairbnbが決めた仕様で設定してくれます。

上で書いたことを全部対応した設定ファイルが下記の `.eslintrc.json` です。

```json
{
    "extends": "airbnb",
    "plugins": [
        "react",
        "jsx-a11y",
        "import"
    ],
    "parser": "babel-eslint",
    "env": {
        "browser": true
    },
    "rules": {
        "react/forbid-prop-types": 0
    }
}
```

### dependencies周り

ReactとMobXを使うことが決まっているので、その周辺ツールを選定しています。[公式のリファレンス](https://mobx.js.org/)みてたら `mobx-react` を使いましょうってなっていたので使用します。

#### Reactについて

- react-dom
- prop-types

react-domはview周り、prop-typesはpropのtypeの型定義するために導入しています。

#### MobXについて

- mobx-react
- mobx-react-devtools

`mobx-react` はstoreをbindするため、`mobx-react-devtools`はデバッグ用で導入しています。

# フローを眺めておく

先に大枠の流れだけ見ておきます。

![MobXのフロー](https://mobx.js.org/docs/flow.png)

雑な認識だとActionにイベントが来てStateを書き換えComputedでStateをもとにした加工を行いViewに反映、Reactionと呼ばれる機能で監視していたStateに変更があった場合にActionにイベントを送るって流れだと思います。

# MobXでstoreを作る

カウントアプリを作るので `CountStore` クラスを作ります。
先程のフローに当てはめるとStateとComputed、Action周りの実装がここでできます。

storeを定義するときには下記の3つを利用します。

- observable
- computed
- action

最終的に下記のようなstoreになりました。

```js
import { observable, computed, action } from 'mobx';

const sleep = msec => new Promise(resolve => setTimeout(resolve, msec));

export default class CountStore {
  @observable num = 0;

  @computed get getDoubleCount() {
    return this.num * 2;
  }

  @action.bound onIncrement() {
    this.num = this.num + 1;
  }

  @action.bound onDecrement() {
    this.num = this.num - 1;
  }

  @action.bound async onAsyncIecrement() {
    await sleep(1000);
    this.onIncrement();
  }
}
```

## observaleについて

observaleとは非同期のデータを扱うオブジェクトみたいです。
CountStoreクラス内でnumは外部から変更を監視したりすることができます。
コンポーネントにstoreをinjectすることで参照できます。(できるけどフロー図を参考にする限りcomputedを用意して上げるべきな気もする)

## computedについて

computedとはobservaleとかでstoreに登録されてるデータを加工してアクセスしたい場合に使います。
コンポーネントにstoreをinjectすることで参照できます。

Reduxを使ってる場合は[reselect](https://github.com/reactjs/reselect)とかを使って実現するような機能だと思います。

## actionについて

action.boundを使うことでFunctionを渡すことができます。
コンポーネントにstoreをinjectして実行することができます。

ReduxのActionと似ていますがtypeを持つオブジェクトをディスパッチに送る必要がなく便利です。

サイドエフェクトをどう書くのか考えていたのですが、普通にasync, awaitで良さそうです。1000ms待ってからonIncrementを実行できるようにしています。

# ComponentにMobXからStateをbindする

よくあるCountコンポーネントを実装してMobXからStateをbindします。

先に完成形を眺めながらのほうがいいと思うので下記にコンポーネントのコード書きます。

```js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { inject, observer } from 'mobx-react';
import DevTools from 'mobx-react-devtools';

@inject('count')
@observer
export default class Counter extends Component {
  static propTypes = {
    count: PropTypes.object.isRequired,
  };

  render() {
    const { count } = this.props;

    return (
      <div>
        Count: {count.num} <br />
        Double count: {count.getDoubleCount} <br />
        <button onClick={count.onIncrement}>+1</button>
        <button onClick={count.onDecrement}>-1</button>
        <button onClick={count.onAsyncIecrement}>After 1000ms +1</button>
        <DevTools />
      </div>
    );
  }
}
```

ここではMobXの話を主にしたいので、`inject` と `observer` について書きます。

DevToolsは単にコンソールとかでどんなactionが実行されたかなどを見ることができるだけなのでこんなものもあるんだなってぐらいで見てください。なくても全然問題ないです。

`observer` とはReact ComponentをリアクティブComponentに変換してくれます。使う側はMobXのstoreをいい感じに使えるようにしてくれるって覚えておけば基本問題ないと思います。

`ingect` はcontextからpropにstateを渡してくれます。実際にはComponentをラップしているようで `Counter.propTypes = { count: PropTypes.object.isRequired };` とか書くと `Failed prop type: The prop ~` 的な感じでエラーが出ます。[ここ](https://github.com/mobxjs/mobx-react#using-proptypes-and-defaultprops-and-other-static-properties-in-combination-with-inject)とかを見る限り `wrappedComponent`
 を使うことで解決するようです。今回はラップされるクラスに直接プロパティとして宣言するようにしました。

# 作ったComponentとStoreをつなげて動作するようにする

Providerを利用してstoreを渡して子要素にCounterコンポーネントを入れて動作できるようにします。

全体は下記です。

```js
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'mobx-react';
import Counter from './components/Counter';
import CountStore from './stores/CountStore';

const stores = {
  count: new CountStore(),
};

render(
  <Provider {...stores}>
    <Counter />
  </Provider>,
  document.getElementById('root'),
);
```

# まとめ

Reduxの冗長な書き方がスッキリしていて良さを感じた。ただ記事には書いてないがコードを読んだ感じはReduxの方がすごくシンプルで何が起きてるのか理解しやすい。僕がリアクティブと言うものを理解していない感が強いが使うだけなら良さそう。冗長な記載はその分何が起きるのか理解し易いと思うのです。デコレータを使ってラップしたり魔法がいたる所に散りばめられていてどう動作するのか混乱することが多々あると思いますがしばらく使ってみようと思います。