MST (mobx-state-tree) の進化系？！ mobx-keystone を始める

[mobx-keystone](https://mobx-keystone.js.org/) をやっていきたいと思います。

- 公式サイト: https://mobx-keystone.js.org/
- GitHub: https://github.com/xaviergonz/mobx-keystone
- 紹介記事: https://medium.com/@xaviergonz/mobx-keystone-an-alternative-to-mobx-state-tree-without-some-of-its-pains-8140767a3aa1

## MST に比べて何がいいのか？

https://mobx-keystone.js.org/mstcomparison/

- 基本的にMSTと同じようにツリー状のJSONと等価なデータでアプリ状態を表す
    - ツリーにメタデータ(`{$modelType: string, $modelId: string}`)が付くようです。 
        - `applySnapshot()` する時もこれらが要求されるのでそこは面倒。
- MSTと違い、（ほぼ）ネイティブなTypeScriptのクラス構造を用いることができる
    - デコレータベースで記述するが、別にそうじゃない方法も用意されている
    - `new Model()` でモデルの生成を行えます。
- MSTと違い、 **モデル間の循環依存を扱うことができる**
    - 個人的にはこれが一番重要な特徴だと思います。MSTだと循環依存でハマる。誰でもハマる。
- [MSTに比べてパフォーマンスもいい](https://github.com/xaviergonz/mobx-keystone/issues/4) そうです

基本的にMSTを踏まえて作られたらしい。MSTの苦痛が減っていると嬉しいですね。

## さっそく始める

セットアップが簡単な [CRA](https://github.com/facebook/create-react-app) で始めます。

```
$ yarn create react-app mks --template typescript
$ cd mks
$ yarn add mobx mobx-react-lite mobx-keystone
```

`tsconfig.json` に下記を追加しておきましょう。

```js:tsconfig.json
    // decoratorを使うため
    "experimentalDecorators": true,

    // ES5がtargetだと非同期モデルアクションの記述で怒られるため
    "downlevelIteration": true,
```

## ものすごく簡単なカウンター

カウンターを例に簡単なモデルを書いてみましょう。

### カウンターのモデル

カウンターのモデルのコードです。

`extends Model({プロパティ定義})` するのと、副作用があるメソッドは `@modelAction` で装飾することを除けば、だいたいMobXの流儀でいけます。（MSTからというより、MobXからの移行する人のほうがすんなり行けるかも？？）

```typescript:Counter.ts
import { Model, prop, model, modelAction } from "mobx-keystone";
import React, { useContext } from "react";
import { computed } from "mobx";

@model("Counter")
export class Counter extends Model({
  count: prop<number>(),
}) {

  @computed
  get doubled() {
    return this.count * 2;
  }

  @modelAction
  incr() {
    this.count++;
  }

  @modelAction
  decr() {
    this.count--;
  }
}


// モデルはコンテキストとして渡しましょう
export const CounterContext = React.createContext<Counter>(
  new Counter({ count: 0 })
);

// モデルを利用するコンポーネント用hook
export const useCounter = () => useContext(CounterContext);
```

### コンテナコンポーネント

カウンターモデルを表示するReactコンポーネントを書きましょう。MobXをReactコンポーネントと連動させるために、 `mobx-react-lite` を使います。

注意点としては、MSTと異なり、メソッドが本当にクラスのメソッドなので、アクションメソッドの中でthisがバインドされていない点です。 `onCick={counter.incr}` のように記述できません。 `useCallback` を使うなり、 `() => counter.incr()` とラムダで包むなりしないといけません。

```typescript:App.tsx
export const App: React.FC<{}> = () => {
  const counter = useCounter();

  const incr = useCallback(() => {
    counter.incr();
  }, [counter]);

  const decr = useCallback(() => {
    counter.decr();
  }, [counter]);

  return useObserver(() => (
    <div>
      <h1>
        {counter.count} を倍にすると {counter.doubled}
      </h1>
      <button onClick={incr}>+</button>
      <button onClick={decr}>-</button>
      {/* モデルをJSONとしても表示してみる */}
      <pre>{JSON.stringify(getSnapshot(counter))}</pre>
    </div>
  ));
};
```

### 動作させてみる

ちゃんと動きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9619/c6c05d0f-768e-f11e-7a41-fa383756a319.png)

## 非同期

こういったモデルプログラミングの上で、非同期処理の扱いは大事ですよね。MSTでは非同期処理の扱いは非常に簡単でした。

mobx-keystoneでも[非常に簡単なようです](https://mobx-keystone.js.org/classModels#flows-async-actions)。試しに指定ミリ秒数、遅延するインクリメントボタンを実装してみましょう。

```typescript:Counter.ts
  @modelFlow
  delayedIncr = _async(function* (this: Counter, delayedMs: number) {
    // yield (単一値のジェネレート) ではなく yield* (複数値のジェネレート) を用いることに注意
    yield* _await(sleep(delayedMs));
    this.incr();
  });

// 上記内に記述されている sleep は以下のような関数です
const sleep = (ms: number) => new Promise((done) => setTimeout(done, ms));
```

`@modelFlow` で修飾したプロパティについて `_async(ジェネレータ関数)` を渡して、ジェネレータ関数内で `yield* _await(...)` すれば非同期処理できるようです。

### スナップショット

JSONデータとモデルを相互変換できないとキツいですよね。素のMobXの主な不満点はここらへんにあります。

基本的には `getSnapshot()` も `applySnapshot()` も存在してます。

```typescript
const modelJson = getSnapshot(model)
applySnapshot(model, modelJson)

// インスタンスすら作れる
const newModel = fromSnapshot(modelJson)
```

が、`applySnapshot` に自作のJSONをMSTの感覚で渡せるかというと、それは違うんですね。メタデータが必要になってきます。

```typescript
applySnapshot(model, {
   modelPropA: xxx,

   $modelType: "Model", 
   $modelId: "何が入るかわからない…"
})
```

メタデータがある点が、MSTに比べてやや面倒です。

## とりあえず今回はここまで

スナップショットの型が不穏という以外は、MSTよりよさそうな感じはします。もっとユーザーが増えるといいなあと思います。

実用的に使うとしたら、下記について網羅する必要があるかなと思います。

- ツリートラバーサル
    - 祖先ノードの参照、子孫ノードの参照
- HMR
- 相互参照
- DI
- 配列・マップ構造
- さらなる型
- カスタム型
- オブジェクトのID