はじめに
この記事ではTypescript(.tsx)でNext.js@canaryのサーバーを立て、MobXの動くものをちょいと作ります。
また、axiosでモックアダプターを作る方法を少し説明します。
そして、GitHubのNoopsChalangeのAPIに接続します。
- これはとりあえず動くものを素早く作るためのチュートリアルです
- Typescriptを手っ取り早く設定するために、執筆時点ではNext.jsのcanaryバージョンを使います
想定読者
以下のいずれか
- TypescriptとReactが読める
- これを読んだ人:なる早でTypescript + Next + MobX + mobx-react-lite でグリグリ動く「買い物リスト」みたいなのを作る
今日作るもの
できたやつ: 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
{
"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
に付いてくる。
{
"presets": [
"next/babel",
"mobx"
]
}
indexページの作成
pages/index.tsx
を作成する。
export default () => (
<h1>It Works!</h1>
);
tsconfig.json
"compilerOptions": {}
のメンバに"experimentalDecorators": true,
があれば良い。
Next.jsのGitHubに書かれている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を使ってみようと思います。
これは至極単純なAPIで、
GET https://api.noopschallenge.com/hexbot
を叩くと
{
"colors": [
{"value": "#52a351"}
]
}
このような形式でランダムなRGB値が帰ってきます。
では早速、axios
でAPIをを使ってみましょう。
まずは型から入ります。
export interface ColorType {
"value": string | null;
}
export interface ColorResponse {
"colors": ColorType[]
}
export const noopURI = "https://api.noopschallenge.com";
export const hexBotEndPoint = "/hexbot";
接続するためのaxios
インスタンスを作ります。
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
型のレスポンスをするアダプタを作る関数です。
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>
の型定義は以下の通りです。
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点の方法の共通部分である非同期処理を必要としない部分を書いてしまいます。
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
するだけでは不足です。以下のように書かなければなりません。
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
を作ってしまう
二つ目のやり方を見ていきましょう。これは単純です。
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とか出てきていますが、やっていることはかなりシンプルです。
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);
に書き換えawait
をyield
に書き換えるだけのことです。ただしJavascript
由来のやたら挙動が不気味なthis
の片鱗が見え隠れして、見た目が少し気持ち悪いです。
参考: async actions & flows | MobX
とにかく、この気持ち悪さはジェネレーター関数にアロー関数表記がない為であり、あきらめましょう。やっていることはシンプルです。
動かす
以上をReactで使ってみるとこうなります。
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;
まとめ
-
axios
のアダプターをTypescript
で型をつけながらか書くのは若干めんどい - MobXは非同期処理をサクッと描ける
- ただし、
configure({enforceActions: "observed"});
をつけて非同期処理を行おうとすると少し気持ち悪い