見出し画像

MobX の使い方

以下の記事が面白かったので、ざっくり翻訳しました。

MobX: Ten minute introduction to MobX and React

1. はじめに

MobX」は、シンプルでスケーラブルな、状態管理ソリューションです。このチュートリアルでは、「MobX」の重要な概念を10分で紹介します。「MobX」はスタンドアロンライブラリですが、ほとんどの人が「React」で使用しています。

2. コアとなるアイディア

「状態」はアプリケーションでもっとも重要なものの1つです。一貫性のない状態や、ローカル変数と同期していない状態は、不具合の多い管理不可能なアプリケーションを生み出します。

多くの状態管理ソリューションは、状態を変更できる方法を制限しようとします。しかし、これは新しい問題を引き起こします。データは正規化する必要があり、参照の整合性は保証されなくなり、プロトタイプなどの強力な概念を使用することができなくなります。

「MobX」は、根本的な問題に対処することで、状態管理を再びシンプルにします。矛盾した状態を生成することを不可能にします。これを達成するための戦略はシンプルです。

アプリケーションの状態から派生するものすべてを自動的に派生させる。

概念的には、「MobX」はアプリケーションを「スプレッドシート」のように扱います。

画像1

(1) 状態(State)
アプリケーションのモデルを形成するオブジェクトです。配列、プリミティブ、参照などを保持します。これらの値はアプリケーションの「データセル」にあたります。

(2) 派生(Derivations)
アプリケーションの状態から自動的に計算できる値です。これら値は、未完了のToDoの数のような単純な値から、ToDoのHTML表示のような複雑なものまであります。スプレッドシート用語で言えば、アプリケーションの「数式」や「チャート」にあたります。

(3) リアクション(Reactions)
「派生」とよく似ています。主な違いは、これらの関数は値を生成しないことです。その代わりに、何らかのタスクを自動的に実行します。通常、これは I/O に関連したものになります。これらの関数は、DOMが更新されたことを確認したり、ネットワークリクエストが適切なタイミングで自動的に行われるようにします。

(4) アクション(Actions)
「状態」を変更するすべてのものです。「MobX」は、「アクション」によって引き起こされるアプリケーションの状態へ変更の全てが、「派生」と「リアクション」によって自動的に処理されるようにします。

3. シンプルなToDoストア

非常にシンプルな「ToDoストア」の例から見てみましょう。
「MobX」は、まだ関与していません。

class TodoStore {
    todos = [];

    // 達成済みToDoの数の取得
    get completedTodosCount() {
        return this.todos.filter(
            todo => todo.completed === true
        ).length;
    }

    // レポートの出力
    report() {
        if (this.todos.length === 0)
            return "<none>";
        const nextTodo = this.todos.find(todo => todo.completed === false);
        return `Next todo: "${nextTodo ? nextTodo.task : "<none>"}". ` +
            `Progress: ${this.completedTodosCount}/${this.todos.length}`;
    }

    // ToDoの追加
    addTodo(task) {
        this.todos.push({
            task: task,
            completed: false,
            assignee: null
        });
    }
}

const todoStore = new TodoStore();

todoStoreにいくつかToDoを追加してみます。変更の効果を確認するために、変更のたびにtodoStore.report()をログ出力します。report()は達成済みでない要素1つのみを出力することに注意してください。

todoStore.addTodo("read MobX tutorial");
console.log(todoStore.report());

todoStore.addTodo("try MobX");
console.log(todoStore.report());

todoStore.todos[0].completed = true;
console.log(todoStore.report());

todoStore.todos[1].task = "try MobX in own project";
console.log(todoStore.report());

todoStore.todos[0].task = "grok MobX tutorial";
console.log(todoStore.report());
Next todo: "read MobX tutorial". Progress: 0/1
Next todo: "read MobX tutorial". Progress: 0/2
Next todo: "try MobX". Progress: 1/2
Next todo: "try MobX in own project". Progress: 1/2
Next todo: "try MobX in own project". Progress: 1/2

4. リアクティブにする

「状態」が変化するたびに、report()を自動的に呼び出されるように宣言する場合はどうすれば良いしょうか。これにより、report()を呼び出す責任から解放されます。

幸いなことに、これが「MobX」でできることです。「状態」のみに依存するコードを自動的に実行します。これを実現するには、「TodoStore」で行われている全ての変更を追跡できるように、「MobX」が監視できるようにする必要があります。

class ObservableTodoStore {
    @observable todos = [];
    @observable pendingRequests = 0;

    // コンストラクタ
    constructor() {
        mobx.autorun(() => console.log(this.report));
    }

    // 達成済みToDoの数の取得
    @computed get completedTodosCount() {
        return this.todos.filter(
            todo => todo.completed === true
        ).length;
    }

    // レポートの出力
    @computed get report() {
        if (this.todos.length === 0)
            return "<none>";
        const nextTodo = this.todos.find(todo => todo.completed === false);
        return `Next todo: "${nextTodo ? nextTodo.task : "<none>"}". ` +
            `Progress: ${this.completedTodosCount}/${this.todos.length}`;
    }

    // ToDoの追加
    addTodo(task) {
        this.todos.push({
            task: task,
            completed: false,
            assignee: null
        });
    }
}

const observableTodoStore = new ObservableTodoStore();

観察対象となるプロパティ「todos」に、「@observable」を付加します。「pendingRequestsassignee」にも付加しましたが、これは現在使用してなく、このチュートリアルの後半で使用します。

派生先となるメソッド「report()」「completedTodosCount()」に「@computed」を付加します。

コンストラクタで、「mbox.autorun()」を使って、監視対象となるプロパティが変更されるたびに自動的に実行する関数を指定します。今回はreport()をログ出力しています。

observableTodoStore.addTodo("read MobX tutorial");
observableTodoStore.addTodo("try MobX");
observableTodoStore.todos[0].completed = true;
observableTodoStore.todos[1].task = "try MobX in own project";
observableTodoStore.todos[0].task = "grok MobX tutorial";
Next todo: "read MobX tutorial". Progress: 0/1
Next todo: "read MobX tutorial". Progress: 0/2
Next todo: "try MobX". Progress: 1/2
Next todo: "try MobX in own project". Progress: 1/2

report()の同期および中間値を漏らすことなく、自動的に出力されました。ログをよくみてみると、5行目が出力されてなかったことがわかります。バッキングデータは変更されましたが、report()の出力は変更されなかったためです。

5. Reactをリアクティブにする

Reactのコンポーネントは(その名前にもかかわらず)そのままではリアクティブではありません。「@observerデコレータ」を付加することで、Reactのコンポーネントのrender()をautorun()でラップし、自動的にコンポーネントを状態と同期するようになります。

次のリストでは、いくつかのReactのコンポーネントを定義しています。ここにある唯一の「MobX」のものは「@observerデコレータ」のみです。これだけで、関連するデータが変更された時に、各コンポーネントが個別に再レンダリングされることを確認できます。

@observer
class TodoList extends React.Component {
  // 描画
  render() {
    const store = this.props.store;
    return (
      <div>
        { store.report }
        <ul>
        { store.todos.map(
          (todo, idx) => <TodoView todo={ todo } key={ idx } />
        ) }
        </ul>
        { store.pendingRequests > 0 ? <marquee>Loading...</marquee> : null }
        <button onClick={ this.onNewTodo }>New Todo</button>
        <small> (double-click a todo to edit)</small>
        <RenderCounter />
      </div>
    );
  }

  // 新規追加
  onNewTodo = () => {
    this.props.store.addTodo(prompt('Enter a new todo:','coffee plz'));
  }
}

@observer
class TodoView extends React.Component {
  // 描画
  render() {
    const todo = this.props.todo;
    return (
      <li onDoubleClick={ this.onRename }>
        <input
          type='checkbox'
          checked={ todo.completed }
          onChange={ this.onToggleCompleted }
        />
        { todo.task }
        { todo.assignee
          ? <small>{ todo.assignee.name }</small>
          : null
        }
        <RenderCounter />
      </li>
    );
  }

  // 達成トグル
  onToggleCompleted = () => {
    const todo = this.props.todo;
    todo.completed = !todo.completed;
  }

  // 名前変更
  onRename = () => {
    const todo = this.props.todo;
    todo.task = prompt('Task name', todo.task) || todo.task;
  }
}

ReactDOM.render(
  <TodoList store={ observableTodoStore } />,
  document.getElementById('reactjs-app')
);

次のリストを見ると、他に何もしなくてもデータを変更するだけで良いことがわかります。「MobX」は自動的に、ストア内の状態からユーザーインターフェイスの関連部分を自動的に派生および更新します。

const store = observableTodoStore;
store.todos[0].completed = !store.todos[0].completed;
store.todos[1].task = "Random todo " + Math.random();
store.todos.push({ task: "Find a fine cheese", completed: true });

6. おわりに

これで、独自のアプリケーションで「mobx」および「mobx-react」を使用する準備ができました。
これまでに学んだことの簡単な要約は次のとおりです。

(1) 「@observableデコレータ」を使用して、「MobX」でオブジェクトを追跡可能にします。
(2) 「@computedデコレータ」を使用して、状態から値を自動的に派生できる関数を作成できます。
(3) 「autorun()」を使用して、観察可能な状態に依存する関数を自動的に実行します。 これは、ロギング、ネットワーク要求の作成などに役立ちます。
(4) mobx-reactパッケージの「@observerデコレータ」を使用して、Reactコンポーネントを本当にリアクティブにします。 これらは自動的かつ効率的に更新されます。

【おまけ】 decorate構文とdecorateユーティリティ

◎ decorator構文

import { observable, computed, action } from "mobx"

class Timer {
    @observable start = Date.now()
    @observable current = Date.now()

    @computed
    get elapsedTime() {
        return this.current - this.start + "milliseconds"
    }

    @action
    tick() {
        this.current = Date.now()
    }
}

◎ decorateユーティリティ

import { observable, computed, action, decorate } from "mobx"

class Timer {
    start = Date.now()
    current = Date.now()

    get elapsedTime() {
        return this.current - this.start + "milliseconds"
    }

    tick() {
        this.current = Date.now()
    }
}
decorate(Timer, {
    start: observable,
    current: observable,
    elapsedTime: computed,
    tick: action
})

この記事が気に入ったらサポートをしてみませんか?