なる早でTypescript + Next + MobX + mobx-react-lite で非同期処理をサクッと扱う

# はじめに
この記事では[Typescript](https://www.typescriptlang.org)(.tsx)で[Next.js@canary](https://nextjs.org)のサーバーを立て、[MobX](https://github.com/mobxjs/mobx)の動くものをちょいと作ります。

また、[axios](https://github.com/axios/axios)でモックアダプターを作る方法を少し説明します。

そして、GitHubの[NoopsChalange](https://noopschallenge.com)のAPIに接続します。
 <a href="https://noopschallenge.com"><img width="400" alt="スクリーンショット 2019-07-01 10.48.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/0fb61e20-eabc-d707-f4ff-9e55ce0941a0.png" ></a>


- これはとりあえず動くものを素早く作るためのチュートリアルです
- Typescriptを手っ取り早く設定するために、執筆時点ではNext.jsの<ruby>canary<rt>カナリヤ</rt></ruby>バージョンを使います


#### 想定読者
以下のいずれか

* TypescriptとReactが読める
* これを読んだ人：[なる早でTypescript + Next + MobX + mobx-react-lite でグリグリ動く「買い物リスト」みたいなのを作る](https://qiita.com/NanimonoDemonai/items/141b99f316190b265821.md)
* 
<details><summary> <font color="#b01">筆者の環境</font> </summary><div>
* **OS:** macOS Mojave 10.14.5
* **ブラウザ:** Safari バージョン12.1.1 
* **[Node.js](https://nodejs.org/ja/):** v10.16.0
* **[Yarn]():** 1.15.2
</div></details>

#### 今日作るもの
![画面収録 2019-07-01 12.06.54.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/20b47ec0-1cc2-43f8-f221-a70d642b9c75.gif)


**できたやつ:** https://github.com/NanimonoDemonai/noopTest/tree/master

# プロジェクトの準備
#### パッケージインストール

```
yarn add react react-dom next@canary mobx mobx-react-lite axios
yarn add -D typescript @types/react @types/react-dom @types/node babel-preset-mobx
```
<font color="#bbb">*/*TODO: Next.js 8.1.1がリリースされたら＠canaryを消す***/*</font>

<details><summary> <font color="#b01">執筆当時のpackage.json</font> </summary><div>

```json:package.json
{
  "dependencies": {
    "axios": "^0.19.0",
    "mobx": "^5.10.1",
    "mobx-react-lite": "^1.4.1",
    "next": "^8.1.1-canary.63",
    "react": "^16.8.6",
    "react-dom": "^16.8.6"
  },
  "devDependencies": {
    "@types/node": "^12.0.10",
    "@types/react": "^16.8.22",
    "@types/react-dom": "^16.8.4",
    "babel-preset-mobx": "^2.0.0",
    "typescript": "^3.5.2"
  }
}

```

</div></details>

#### 各種configの作成
以下のファイルを```package.json```と同階層に作成してください。

**.babelrc**

あとあと、クラスデコレータを使うので".babelrc"を作成して、編集しておく、`preset:"next/babel"`は`next@canary`に付いてくる。

```json:.babelrc
{
  "presets": [
    "next/babel",
    "mobx"
  ]
}
```

**indexページの作成**

`pages/index.tsx`を作成する。

```tsx:pages/index.tsx
export default () => (
   <h1>It Works!</h1>
);
```

**tsconfig.json**
```  "compilerOptions": {}```のメンバに```"experimentalDecorators": true,```があれば良い。

Next.jsの[GitHub](https://github.com/zeit/next.js#typescript)に書かれている`tsconfig.json`にこのオプションを挿した例はこれ。

```js:tsconfig.json
{
  "compilerOptions": {
    "allowJs": true, /* Allow JavaScript files to be type checked. */
    "alwaysStrict": true, /* Parse in strict mode. */
    "esModuleInterop": true, /* matches compilation setting */
    "isolatedModules": true, /* to match webpack loader */
    "jsx": "preserve", /* Preserves jsx outside of Next.js. */
    "lib": ["dom", "es2017"], /* List of library files to be included in the type checking. */
    "module": "esnext", /* Specifies the type of module to type check. */
    "moduleResolution": "node", /* Determine how modules get resolved. */
    "noEmit": true, /* Do not emit outputs. Makes sure tsc only does type checking. */

    "experimentalDecorators": true, /* ここに挿した */

    /* Strict Type-Checking Options, optional, but recommended. */
    "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
    "noUnusedLocals": true, /* Report errors on unused locals. */
    "noUnusedParameters": true, /* Report errors on unused parameters. */
    "strict": true /* Enable all strict type-checking options. */,
    "target": "esnext" /* The type checking input. */
  }
}
```

#### 今回のファイル構成

```
.
├── .next/             ⇦ 勝手にできる
├── node_modules/      ⇦ 勝手に色々入っている
├── pages
│   └── index.tsx      ⇦ さっき作った
├── store 
│   ├── api.ts         ⇦ これから作る
│   └── colorStore.ts  ⇦ これから作る
├── next-env.d.ts      ⇦ 勝手にできる
├── package.json       ⇦ 勝手にできる
├── .babelrc           ⇦ さっき作った
├── tsconfig.json      ⇦ 自分で作るか、勝手にできたのをいじる
└── yarn.lock          ⇦ 勝手にできる


```
#### 開発用サーバの起動

以下のコマンドを実行すると

```
yarn next
```

以下にサーバが立つ
http://localhost:3000

# Hexbotと接続用のAPIを作る

今回は、GitHubが公開しているの[NoopsChalange](https://noopschallenge.com)の中の[Hexbot](https://noopschallenge.com/challenges/hexbot)を使ってみようと思います。
 <a href="https://noopschallenge.com/challenges/hexbot"><img width="200" alt="スクリーンショット 2019-07-01 10.49.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/63744cb3-d187-8d1d-dfb4-90f61f219655.png" ></a>

これは至極単純なAPIで、

```
GET https://api.noopschallenge.com/hexbot
```

を叩くと

```json
{
  "colors": [
    {"value": "#52a351"}
  ]
}
```

このような形式でランダムなRGB値が帰ってきます。

では早速、`axios`でAPIをを使ってみましょう。

まずは型から入ります。

```store/api.ts
export interface ColorType {
    "value": string | null;
}

export interface ColorResponse {
    "colors": ColorType[]
}

export const noopURI = "https://api.noopschallenge.com";
export const hexBotEndPoint = "/hexbot";
```

接続するための`axios`インスタンスを作ります。

```store/api.ts
import axios from "axios"; //追記

//型中略

export const noopAPI = axios.create({
    baseURL: noopURI,
    headers: {
        'Content-Type': 'application/json',
        'X-Requested-With': 'XMLHttpRequest'
    },
    responseType: 'json'
});
```

あとはモックアダプタを作りましょう。

以下は`MockData`型と`watiTime:number`を受け取って、`watiTime`ミリ秒待って`MockData`型のレスポンスをするアダプタを作る関数です。

```store/api.ts
import axios, {AxiosAdapter, AxiosResponse} from "axios"; //追記

//中略

export interface MockData {
    color: string | null;
    waitTime?: number;
}

export const mockAdapterCreator: (data: MockData) => AxiosAdapter
    = data => async config => {

    const initializer: Required<MockData> = {
        ...{
            color: null,
            waitTime: 0
        }, ...data
    };

    //sleep タイマー
    await new Promise(resolve => setTimeout(resolve, initializer.waitTime));

    const mockData: ColorResponse = {
        colors: [{
            value: initializer.color
        }]
    };

    const response: AxiosResponse = {
        data: mockData,
        status: 200,
        statusText: "",
        headers: "",
        config: config,
    };

    return response;
};
```

モックAPIは`axios`のアダプタで作ります。`axios`のアダプタは`AxiosAdapter`型です。

`AxiosAdapter`は`config: AxiosRequestConfig`を受け取って、`Promise<AxiosResponse <any>>;`を返す関数の型です。

また`AxiosResponse<T>`の型定義は以下の通りです。

```tsx:
export interface AxiosResponse<T = any>  {
  data: T;
  status: number;
  statusText: string;
  headers: any;
  config: AxiosRequestConfig;
  request?: any;
}
```

これらの使い方は、次節で説明します。

# 非同期処理を行うMobXのstoreを作る

MobXで非同期処理を扱う方法は何点かありますが、今回は`configure({enforceActions: "observed"});`の下で、非同期処理をする方法を3点紹介します。まずは、3点の方法の共通部分である非同期処理を必要としない部分を書いてしまいます。

```store/colorStore.ts
import {observable, configure, flow, action, runInAction, computed} from "mobx";
import {ColorResponse, ColorType, hexBotEndPoint, mockAdapterCreator, MockData, noopAPI} from "./api";
import {AxiosAdapter, AxiosResponse} from "axios";

configure({enforceActions: "observed"});

export enum ColorTypeStatus {
    init = "初期値",
    fetching = "フェッチ中",
    fetched = "フェッチ完了"
}

export abstract class AbstractColorAPI {
    @observable protected _color: ColorType = {value: "null"};
    @observable protected _status: ColorTypeStatus = ColorTypeStatus.init;
    private _adapter: AxiosAdapter | null;

    constructor(data?: MockData) {
        this._color = {value: "null"};
        this._status = ColorTypeStatus.init;
        this._adapter = data ? mockAdapterCreator(data) : null;
    }

    @computed get color() {
        return this._color;
    }

    @computed get status() {
        return this._status;
    }

    get isMock() {
        return this._adapter != null;
    }

    setMock(data: MockData) {
        this._adapter = mockAdapterCreator(data);
    }

    unSetMock() {
        this._adapter = null;
    }

    protected async get(): Promise<AxiosResponse<ColorResponse>> {
        return this._adapter != null
            ? noopAPI.get(hexBotEndPoint, {adapter: this._adapter})
            : noopAPI.get(hexBotEndPoint);
    }

    //ここを実装する
    abstract fetchColor(): void;

    @action.bound
    refetch() {
        this._status = ColorTypeStatus.init;
        this.fetchColor();
    }
}
```
先ほど実装したモックアダプタは`get()`で使われています。これで`axios`インスタンスは実際のapiの返答の代わりに、`MockData`を返します。

では、非同期処理の```abstract fetchColor(): void;```を実装するクラスを書いていきます。

## runInAction(fn)を使う
内容を見ていきましょう。

まず、`MobX`はasyncを普通に書いても動きます。また、`configure({enforceActions: "observed"});`がない場合ならば、以下のように書くことができます。もちろん、その`configure`を使わないならば、`@action`すら不要です。

```ts
//configure({enforceActions: "observed"});がないとき
export class ColorAPI extends AbstractColorAPI {
    async fetchColor() {
        if (this._status != ColorTypeStatus.init) {
            throw new Error("フェッチ中か、フェッチ終わっとるわ");
        }
        this._status = ColorTypeStatus.fetching;

        const response = await this.get();

        this._status = ColorTypeStatus.fetched;
        this._color = {value: response.data.colors[0].value}

    };
}
```

しかし、`configure({enforceActions: "observed"});`があるときは、上記の例に単に`@action.bound`するだけでは不足です。以下のように書かなければなりません。

```store/colorStore.ts
export class ColorAPI1 extends AbstractColorAPI {
    @action.bound
    async fetchColor() {
        if (this._status != ColorTypeStatus.init) {
            throw new Error("フェッチ中か、フェッチ終わっとるわ");
        }
        this._status = ColorTypeStatus.fetching;

        const response = await this.get();

        runInAction(() => {
            this._status = ColorTypeStatus.fetched;
            this._color = {value: response.data.colors[0].value}
        })
    };
}
```
最初の`await`の以降で`@observable`な値を触ると、たとえ`@action.bound`の中にあっても、`@action`の外からの変更として`MobX`から怒られてしまいます。

なので、インラインでアクションとして実行する`runInAction(fn)`を使わなければなりません。

```ts
runInAction(() => {
   /*ここでの処理はActionとみなされ実行される即座に*/
})
```

## 新しく`@action`を作ってしまう
二つ目のやり方を見ていきましょう。これは単純です。

```store/colorStore.ts
export class ColorAPI2 extends AbstractColorAPI {
    @action.bound
    async fetchColor() {
        if (this.status != ColorTypeStatus.init) {
            throw new Error("フェッチ中か、フェッチ終わっとるわ");
        }

        this.setStatus(ColorTypeStatus.fetching);

        const response = await this.get();
        this.setStatus(ColorTypeStatus.fetched);
        this.setColor({value: response.data.colors[0].value});
    };

    @action.bound
    private setColor(color: ColorType) {
        this._color = color;
    }

    @action.bound
    private setStatus(status: ColorTypeStatus) {
        this._status = status;
    }
}
```

`await`の次に、`@observable`な値を触ることを見越して、`@action`を作っておけば、それで機能します。ただし、`@action`を追うようなデバッグをした時に、ログが見づらくなるかもしれません。ただしそれでも、`runInAction(fn)`よりはマシな気がします。

## flow(*fn)を使う
3つ目のやり方はジェネレータ関数を使っており、bindとか出てきていますが、やっていることはかなりシンプルです。

```store/colorStore.ts
export class ColorAPI3 extends AbstractColorAPI {
    fetchColor = flow(function* (this: ColorAPI3) {
        if (this._status != ColorTypeStatus.init) {
            throw new Error("フェッチ中か、フェッチ終わっとるわ");
        }
        this._status = ColorTypeStatus.fetching;


        const response = yield this.get();

        this._status = ColorTypeStatus.fetched;
        this._color = {value: response.data.colors[0].value}
    }).bind(this);
}
```

簡単に言えば

```ts
@action.bound
    async fetchColor() {
    /*ここに処理を書く*/
    }
```

を

```ts
    fetchColor = flow(function* fetchColor(this: クラス名) {
    /*ここに処理を書く*/
    }).bind(this);
```

に書き換え`await`を`yield`に書き換えるだけのことです。ただし`Javascript`由来のやたら挙動が不気味な`this`の片鱗が見え隠れして、見た目が少し気持ち悪いです。
**参考:** [async actions & flows | MobX](https://mobx.js.org/best/actions.html)

とにかく、この気持ち悪さはジェネレーター関数にアロー関数表記がない為であり、あきらめましょう。やっていることはシンプルです。

# 動かす

以上をReactで使ってみるとこうなります。

```tsx:pages/index.tsx
import * as React from 'react'
import {Observer} from "mobx-react-lite";
import {AbstractColorAPI, ColorAPI1, ColorAPI2, ColorAPI3, ColorTypeStatus} from "../store/colorStore";
import {FC} from "react";

const api1 = new ColorAPI1();
const api2 = new ColorAPI2();
const api3 = new ColorAPI3();

const mockData1 = {color: "#000000", waitTime: 1000};
const mockData2 = {color: "#DA291C", waitTime: 2000};
const mockData3 = {color: "#E6C414", waitTime: 500};

const mockApi1 = new ColorAPI1(mockData1);
const mockApi2 = new ColorAPI2(mockData2);
const mockApi3 = new ColorAPI3(mockData3);

const APIViewer: FC<{ controller: AbstractColorAPI; }> = props => (
    <Observer>{() =>
        <div>
            <p style={{
                color: props.controller.color.value ? props.controller.color.value : "transparent"
            }}
            >██████ {props.controller.color.value} ██████</p>
            <p>{props.controller.status}</p>
            <button disabled={props.controller.status != ColorTypeStatus.init}
                    onClick={props.controller.fetchColor}>
                おす
            </button>
            <button disabled={props.controller.status != ColorTypeStatus.fetched}
                    onClick={props.controller.refetch}>
                再取得
            </button>
        </div>
    }</Observer>
);

const Index = () => (
    <div>
        <h2>モック</h2>
        <p>データ：{JSON.stringify(mockData1)}</p>
        <APIViewer controller={mockApi1}/>
        <p>データ：{JSON.stringify(mockData2)}</p>
        <APIViewer controller={mockApi2}/>
        <p>データ：{JSON.stringify(mockData3)}</p>
        <APIViewer controller={mockApi3}/>
        <hr/>
        <h2>ほんまもの</h2>
        <APIViewer controller={api1}/>
        <APIViewer controller={api2}/>
        <APIViewer controller={api3}/>
    </div>
);

export default Index;
```
http://localhost:3000

![画面収録 2019-07-01 12.06.54.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/20b47ec0-1cc2-43f8-f221-a70d642b9c75.gif)

# まとめ
* `axios`のアダプターを`Typescript`で型をつけながらか書くのは若干めんどい
* MobXは非同期処理をサクッと描ける
* ただし、`configure({enforceActions: "observed"});`をつけて非同期処理を行おうとすると少し気持ち悪い