LoginSignup
5
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-04-13

mobx-keystone をやっていきたいと思います。

MST に比べて何がいいのか?

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

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

さっそく始める

セットアップが簡単な CRA で始めます。

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

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

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

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

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

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

カウンターのモデル

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

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

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() とラムダで包むなりしないといけません。

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

非同期

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

mobx-keystoneでも非常に簡単なようです。試しに指定ミリ秒数、遅延するインクリメントボタンを実装してみましょう。

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() も存在してます。

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

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

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

applySnapshot(model, {
   modelPropA: xxx,

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

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

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

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

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

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