LoginSignup
8
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-07-01

はじめに

この記事ではTypescript(.tsx)でNext.js@canaryのサーバーを立て、MobXの動くものをちょいと作ります。

また、axiosでモックアダプターを作る方法を少し説明します。

そして、GitHubのNoopsChalangeのAPIに接続します。
スクリーンショット 2019-07-01 10.48.47.png

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

想定読者

以下のいずれか

今日作るもの

画面収録 2019-07-01 12.06.54.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

/*TODO: Next.js 8.1.1がリリースされたら@canaryを消す*/

執筆当時のpackage.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"
  }
}

各種configの作成

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

.babelrc

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

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

indexページの作成

pages/index.tsxを作成する。

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

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

Next.jsのGitHubに書かれているtsconfig.jsonにこのオプションを挿した例はこれ。

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の中のHexbotを使ってみようと思います。
スクリーンショット 2019-07-01 10.49.00.png

これは至極単純なAPIで、

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

を叩くと

{
  "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型です。

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

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

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すら不要です。

//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)を使わなければなりません。

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);
}

簡単に言えば

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

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

に書き換えawaityieldに書き換えるだけのことです。ただしJavascript由来のやたら挙動が不気味なthisの片鱗が見え隠れして、見た目が少し気持ち悪いです。
参考: async actions & flows | MobX

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

動かす

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

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;

画面収録 2019-07-01 12.06.54.gif

まとめ

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
1