Edited at

今から始めるReact入門 〜 Mobx 編


目次

※MobX の説明については不要で早くデモを始めたい場合はMobX プロジェクトの作成から始めてください


MobX について

Redux を使うことで状態管理を実装することができることを説明しましたが、他の選択肢としてRedux の代わりにMobX を利用することもできます。

MobX もRedux 同様、状態管理をするためのライブラリですが、以下のような違いがあります。


Observable モデル

MobX では状態管理としてObservable を使ってDOM イベント、インターバルタイマー、ソケットのようなpush-based データを利用し、それを簡潔な記述で実現します。

observable なデータを変更(push)するとsubscriber が呼ばれ処理が実行される形式で非同期処理を実行するときにも使え、更にfilter を使用することで特定のデータ条件下の場合のみsubscriber を呼ぶような処理を実装することができます。

さらにMobX ではobservable なデータを更新するとそれに依存して、mutation(≒処理)が起動するようになっており、Redux にあるdispatch 関数を呼ぶスタイルとは違うものがあります。

別の見方をすればRedux ではState を変更したい場合はdispatch を呼び出す形で、MobX ではobservable なデータを更新してmutate を起動すれば良いことになります。


ボイラープレートの削減

Redux よりもボイラープレートが少ない傾向にあります。

コンポーネントや処理が増える度に、同じようなソースコードを幾つも書く煩わしさがありません。


state の標準化

Redux はAction type とその値としてpayload というキーでオブジェクトを渡してデータ構造を標準化していました(厳密には、Redux においてpayload というキーの名前自体は任意です)。

一方でMobX は標準化しないのが一般的になります。

その理由はMobX は値として計算済みの値を持っているということをコンセプトにしているためです。


学習曲線(コスト)

MobX の方が良いと言われている傾向があります。

Redux は思想こそ簡単ではあるが、それを実現するためにあらゆる書き方、immutability などを考慮しないといけません。

一方で学習を続ければRedux の方がカスタマイズ性があり、より多くのことができる可能性は秘めています。

またテスタブルなほうもRedux と言われています。ここわバランスが難しいところです。


ボイラープレート

MobX の方が少ない傾向にあります。

そのため完成したソースコードが簡潔で可読性に優れています。


開発ツール

MobX はRedux と比較してコミュニティの活発度という点でやや劣っているという点で開発ツールについてはRedux より少ない傾向にあります。


内部的なところでエラーが発生した場合のデバッグしやすさ

MobX は開発者によるコード量を減らす傾向にあるので内部的な部分でエラーが発生した場合、それを追跡しにくい傾向にあります。

自動的に計算される値や自動的に起動される関数等も存在するため、プログラマがエラーを追跡しようとするとブラックボックス化されている部分があり難しい点が出てくることもあります。

一方でRedux はお決まりなご作法で作成するプログラム(ボイラープレート)があるので、変数の状態遷移などを追いやすい傾向にあります。

また状態の管理を1 つのStore で管理することを徹底しているので、状態がどのように変わっていくかを追いやすいといった点もあります。


状態の更新ロジック

MobX ではaction を通じて状態を更新することもできますし、画面からpush を直接呼ぶ形でも状態を変更することができます。

一方Redux ではreducer を通じて状態を更新することが決まっているので、開発者が不正なやり方で直接状態を変更できるといったことが起こりずらいです。

この違いからも、Redux の方がバグが発見された場合でも、原因を追跡しやすくなっていると言えるでしょう。


predictability(状態の予測性)

MobX は状態の更新方法が1 つに統一されていません。そのため様々な方向から状態の更新処理が発行され、且つStore が1 つとは限らないため、View はどのStore から状態の変更が飛んでくるか予測をつけづらいです。

Redux は状態の変更はaction の発行、すなわちdispatch することでのみ状態を変更することができます。またRedux は1 つのStore のみを持つのでView はその1 つのStore から状態の変更が飛んでくることに注視していればよいのです。


テスト性

Redux はopinionated (意固地)な傾向がMobX と比較して強いです。

そのため、Action に対するテスト、reducer に対するテストとテストの責任範囲を分割しやすいです。

Action に対するテストは、そのメソッドに特定のデータを渡してAction を起動した後に想定しているPlain Object (例: {type: ADD_USER, payload: { user }} のようなオブジェクト) が発行されたかを確認すれば良く、reducer に対するテストは特定のaction を渡して処理を起動した後に、想定しているstate が発行されたかを確認すれば良いです。

Redux ではこのようにパターン化できるので、コピー&ペーストでどんどんテストケースが作成できてしまうのが特徴です。

一方でMobX はaction から状態を渡すといったことが統一されていないため、例えばpush を直接呼んで状態変更がされようとした場合は、別のパターンのテストケースを書かなければいけなくなったりといった事態が起こりえます。


Modularity

Redux はState をGlobal 領域に保持しています。

一方でMobX は分割されたドメインクラスとしてState を保持しています。それぞれに複数のState オブジェクトが存在しうるということです。

Global 領域にState が管理されるということは各コンポーネントから簡単にアクセスできるため開発がやりやすいですが、一方でアプリケーションが複雑且つ巨大になってきた時に、しっかりと状態をオブジェクトで分割できたほうがやりやすい場合があります。

たとえばある画面のHeader にはそれ用のState, 中央のにはそれ用のState, 中央左にはそれ用のState... というように分けたほうが管理する側としてはやりやすくなる場合もあります。


Scalability / Maintainability

Mobx
Redux

No strict-order
Strict-order

Scattered mutations
Changes centralized

Redux はpredictable なフレームワークであるため、スケールアウトするのには向いていると感じます。


community

今現在だとRedux の方がコミュニティが大きく、情報が多いです。

コミュニティが大きく情報が多いほうが、あなたが直面した問題点はGoogle で検索して解決できる可能性が大幅に広がるかもしれません。


MobX を選ぶかRedux を選ぶかまとめ

Mobx
Redux

シンプルなアプリケーションを作成したい
複雑なアプリケーションを作成したい

試作型を一刻も早く作りたい
じっくり安全に開発したい

小さなチームで使う
大規模なチームで開発したい


どういったアプリケーションに適しているか


MobX


  • リアルタイムシステム、ダッシュボード

  • テキストエディタ、プレゼンテーションソフトウェア

  • イベントベースではない状態の更新を必要とするアプリケーション


Redux


  • ビジネスアプリケーション

  • イベントベースなシステム

  • 複雑な反応を含むゲームのイベント


MobX とRedux の特徴まとめ

Mobx
Redux

state 参照の透過性
時々derivation を通じてstate を更新
Event を通じて起動しない。Observable によるデータ変更
常にreducer を通じて実施
常にimmutable を意識

リアルタイムな反応性
(Reactivity)
Yes: observable を使ったリアルタイムな反応性
No: action ベース


共通している点


  • どちらも自由に使える

  • View レイヤのフレームワークとは分割されている

  • Redux からMobX への移行、MobX からRedux への移行も、まあできる(状況次第では簡単とはならないが)

これらの特徴を理解した上で、次からは実際にMobX を使ったプログラミングを実施していきたいと思います。


MobX プロジェクトの作成

State の管理としてプレインなオブジェクトを使って状態管理を実現しているRedux の代わりに、obserbable を使って状態管理を実現しているMobX を使ってReact アプリケーションを作成してみましょう。

MobX を使うことで、少ないボイラープレートでよりリアルタイム(reactive) なアプリケーションを作成することができます。

まずはプロジェクトの作成と基本的なパッケージのインストールを行います。

observer モデルを使用するためにJavaScript のdecorator を使用するので、@babel/plugin-proposal-decorators パッケージのインストールも忘れずしておきましょう。


Projectの初期化

$ mkdir react-mobx

$ cd react-mobx
$ npm init . -y
$ npm install --save-dev @babel/core @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/preset-env @babel/preset-react \
babel-loader babel-plugin-react-html-attrs \
webpack webpack-cli webpack-dev-server

次にreact とMobX に関連するパッケージをインストールします。

また、css をjavascript からimport するためのパッケージcss-loader, style-loader もインストールします。


react,MobXをインストール

$ npm install --save-dev css-loader style-loader

$ npm install --save-dev react react-dom mobx mobx-react

package.json に以下の1 行を追記します。


package.json

  ......

"scripts": {
"start": "webpack-dev-server --content-base src --mode development --inline",
"test": "echo \"Error: no test specified\" && exit 1"
},
......

次にwebpack.config.js ファイルを作成します。


webpack.conf.js

var debug   = process.env.NODE_ENV !== "production";

var webpack = require('webpack');
var path = require('path');

module.exports = {
context: path.join(__dirname, "src"),
entry: "./js/client.js",
module: {
rules: [{
test: /\.jsx?$/,
exclude: /(node_modules|bower_components)/,
use: [{
loader: 'babel-loader',
options: {
plugins: [
'react-html-attrs',
[require('@babel/plugin-proposal-decorators'), {legacy: true}],
[require('@babel/plugin-proposal-class-properties'), {loose: true }]
],
presets: ['@babel/preset-react', '@babel/preset-env']
}
}]
},
{
test: /\.css$/,
loader: "style-loader!css-loader"
}]
},
output: {
path: __dirname + "/src/",
filename: "client.min.js",
publicPath: '/'
},
devServer: {
historyApiFallback: true
},
plugins: debug ? [] : [
new webpack.optimize.OccurrenceOrderPlugin(),
new webpack.optimize.UglifyJsPlugin({ mangle: false, sourcemap: false }),
],
};


今までのwebpack.config.js に加えて


webpack.config.js

    ......

{
test: /\.css$/,
loader: "style-loader!css-loader"
}
......

の記述を追加してcss がロードできるようにしてください。

その他、今回は以下の作りかけのアプリケーションを配置し、そこからMobX を使ったプログラミングを説明していきたいと思います。

サンプルファイルは以下のGitHub にありますので、参考になればと思います。


ファイル構成

+- src/

| +- css
| | +- index.css
| +- js
| | +- TodoList.js
| | +- TodoStore.js
| | +- index.js
| +- index.html
+- package.json
+- webpack.config.js


サンプルコードの確認

まずは初期の状態のソースコードを見ていきましょう。

今回参考にするのはTodo リストを表示するアプリケーションで初期の状態を確認していきましょう。


src/js/main.js

import "../css/index.css";

import React from "react";
import ReactDOM from "react-dom";

import TodoList from "./TodoList";

const app = document.getElementById("app");

ReactDOM.render(<TodoList />, app);


初期の状態は単にTodoList コンポーネントを表示するだけのアプリケーションです。

TodoList の中はどのようになっているか見てみましょう。


src/js/TodoList.js

import React from "react";

export default class TodoList extends React.Component {
render() {
return <h1>MobX</h1>;
};
}


今の状態ではMobX と表示するだけのコンポーネントです。

React_ReactMobX0000.png

次にsrc/js/TodoStore.js のファイルを見てみましょう。


src/js/TodoStore.js


このファイルには、まだ何も書いてありません。

今回はMobX の状態管理を体験するために、このTodoStore.js の作成をメインプログラミングを実施していきたいと思います。


プログラミングの開始

それではプログラミングを開始していきましょう。

ここからは常にnpm start を実行してWeb ブラウザでhttp://localhost:8080 へアクセスし、開発者モードを開いた状態で実施していきます。

ソースコードについてですが、まずはsrc/js/client.js からTodoStore.js をimport します。


src/js/client.js

 import "../css/index.css";

import React from "react";
import ReactDOM from "react-dom";

import TodoList from "./TodoList";
+import store from "./TodoStore";

const app = document.getElementById("app");

ReactDOM.render(<TodoList />, app);


次にsrc/js/TodoStore.js ファイルを開きます。

まずはMobX からobserbable をimport します。


src/js/TodoStore.js

+import { observable } from "mobx";


次にクラスを作成します。

そのクラスのメンバ変数としてtodos を作成しますが、その変数はobservable として宣言し、observer の監視対象として設定することができます。


src/js/TodoStore.js

 import { observable } from "mobx";

+
+class TodoStore {
+ @observable todos = ["buy milk", "buy eggs"];
+ @observable filter = "";
+}

次に、このクラスをexport します。

export する前に後ほどWeb ブラウザのコンソールでdebug するために、グローバル領域(window) にもこのクラスのインスタンスを保持するようにしておきます。


src/js/TodoStore.js

 import { observable } from "mobx";

class TodoStore {
@observable todos = ["buy milk", "buy eggs"];
@observable filter = "";
}
+
+var store = window.store = new TodoStore();
+
+export default store;


これで状態遷移に対する記述は完了なのですが、実際に状態が遷移しているのを確認したい場合はautorun 関数を使用してState が変更されたタイミングをトリガーにリアルタイムに処理を走らせ、その中にconsole.log() 関数で内容を出力させることができます。


src/js/TodoStore.js

-import { observable } from "mobx";

+import { autorun, observable } from "mobx";

class TodoStore {
@observable todos = ["buy milk", "buy eggs"];
@observable filter = "";
}

var store = window.store = new TodoStore();

export default store;
+
+autorun (() => {
+ console.log(store.filter);
+ console.log(store.todos[0]);
+});


この状態でコンソールを確認してみます。するとこの段階ではまだbuy milk のみ表示される状態です。

React_ReactMobX0000.gif

ここで先程、検証のためにstore をwindow に格納してグローバル領域に公開しましたのでそれを確認してみましょう。

console からstore.filter = "milk";を入力してみます。

React_ReactMobX0001.gif

store.filter の値が変更され、それと同時にautorun が実行されたのが確認できました。

続いてコンソールにstore.todos[0] = "buy cheese"; と入力してstore.todosの値を変更してみましょう。

React_ReactMobX0002.gif

するとまた、store.todos の値が変更されると同時にautorun が実行され、filter とstore の内容が出力されました。

次に生のstore.todos, store.filter を確認してみましょう。

React_ReactMobX0003.gif

> store.todos

Proxy {0: "buy cheese", 1: "buy eggs", length: 2, Symbol(mobx administration): ObservableArrayAdministration}
> store.filter
"milk"

すると、store.todos はObservable な配列として属していることがわかります。

このようにしてstore.todos が変更されることによって、常にこの配列の状態の変更が監視され、変更があった場合は即時autorun 処理が実行されるようになるのです。

一方でstore.filter は一見すると、単なるString なデータ型に見えます。

しかしこれはES6 のobservable の記法でsetter とgetter が利用されているため、setter による状態変化をリアルタイムに検知することができるようになっているのです。

MobX の動きがだいたいわかったところで、次はこれをReact とどのように繋げていけばよいのでしょうか?

答えは特に難しく考えることもなく、このautorun を実行するところでReact のレンダリング処理を呼び出すようにしてあげれば良いのです。

それではautorun と同等の処理をReact に埋め込んでいきましょう。

まずは不要になったautorun メソッドをsrc/js/TodoStore.jsから削除しましょう。


src/js/TodoStore.js

-import { autorun, observable } from "mobx";

+import { observable } from "mobx";

class TodoStore {
@observable todos = ["buy milk", "buy eggs"];
@observable filter = "";
}

var store = window.store = new TodoStore();

export default store;
-
-autorun (() => {
- console.log(store.filter);
- console.log(store.todos[0]);
-});


autorun の代わりにprops でstate を渡すことで、React でリアルタイムにコンポーネントを更新をするようにします。

そのために、上記の変更に加えてsrc/js/client.js 内のTodoList コンポーネントにstore を追加します。


src/js/client.js

 import "../css/index.css";

import React from "react";
import ReactDOM from "react-dom";

import TodoList from "./TodoList";
import store from "./TodoStore";

const app = document.getElementById("app");

-ReactDOM.render(<TodoList />, app);
+ReactDOM.render(<TodoList store={store} />, app);


次にTodoList.js にobserver を追加してobserver デコレータをTodoList コンポーネントに追加します。


src/js/TodoList.js

 import React from "react";

+import { observer } from "mobx-react";

+@observer
export default class TodoList extends React.Component {
render() {
return <h1>MobX</h1>;
};
}

これでstore に対して、React のrender メソッド内からアクセスできるようになりました。

では実際にstore に格納されているtodos の先頭の値が表示されるかを確認してみましょう。


src/js/TodoList.js

 import React from "react";

import { observer } from "mobx-react";

@observer
export default class TodoList extends React.Component {
render() {
- return <h1>MobX</h1>;
+ return <h1>{this.props.store.todos[0]}</h1>;
};
}


TodoList.js を変更したら上書き保存してWeb ブラウザから確認してみましょう。

React_ReactMobX0001.png

するとstore の内容が画面に表示されていることが確認できます。

ここで、Chrome の開発者モードのconsole から、グローバル変数に格納したstore の値を変更してみましょう。


console

> store.todos[0] = "get milk";


React_ReactMobX0003_02.gif

すると画面の表示も自動的に、瞬時に変更されました。

どうでしょう?

MobX とReact を接続するとstate を変更するだけで簡単に自動的に且つリアルタイムに画面の描写を変更することができます。

次からはMobX のfiltering, clearing を使ってTodo リストアプリケーションを作成していきたいと思います。


TODO アプリで学ぶMobX

TODO アプリを実際に作成していく過程を通じてMobX について勉強していきましょう。

src/js/TodoList.js ファイルを編集し、ul タグでtweet のリストを表示するように編集してみましょう。


src/js/TodoList.js

 import React from "react";

import { observer } from "mobx-react";

@observer
export default class TodoList extends React.Component {
render() {
- return <h1>{this.props.store.todos[0]}</h1>;
+ const { todos } = this.props.store;
+
+ const todoList = todos.map(todo => (
+ <li>{todo}</li>
+ ));
+ return <div>
+ <h1>todos</h1>
+ <ul>{todoList}</ul>
+ </div>;
};
}


React_ReactMobXApp0000.png

まだこの時点ではコンソールにWarning: Each child in an array or iterator should have a unique "key" prop. と警告が出ているかもしれませんが、これは後ほど取り除いていくので今は気にしないでください。

次に触るMobX の機能としては、MobX の大きな特徴のひとつであるComputed value についてです。

今回の例ではTodoStore.js 内にあるtodos を画面に表示するアプリケーションですが、filter を定義して、特定の算出済みの結果のみを画面に表示することができるようになります。

今回具体的に実施していく内容としてはtweet のリストから特定の文字列に部分一致するtweet のみを画面に表示するように、プログラムを改修していきます。

まずはfilter を画面からリアルタイムで設定できるようにフォームを作成し、filter がリアルタイムに変更できていることを確認するために処理を追加します。


src/js/TodoList.js

 import React from "react";

import { observer } from "mobx-react";

@observer
export default class TodoList extends React.Component {
+ filter(e) {
+ this.props.store.filter = e.target.value;
+ }
+
render() {
- const { todos } = this.props.store;
+ const { filter, todos } = this.props.store;

const todoList = todos.map(todo => (
<li>{todo}</li>
));
return <div>
<h1>todos</h1>
+ <div>{filter}</div>
+ <input class="filter" value={filter} onChange={this.filter.bind(this)} />
<ul>{todoList}</ul>
</div>;
};
}


画面を開き、フォームに文字を入力してみましょう。

React_ReactMobX0003_03.gif

するとフォームに入力した値がリアルタイムに表示されます。

次はcomputed value (算出値) について実装を進めていきましょう。

src/js/TodoStore.js を開き

mobx からcomputed をインポートします。


src/js/TodoStore.js

-import { observable } from "mobx";

+import { computed, observable } from "mobx";

class TodoStore {
@observable todos = ["buy milk", "buy eggs"];
@observable filter = "";
+ @computed get filterdeTodos() {
+ // do stuff...
+ }
}

var store = window.store = new TodoStore();


上記のように@computed デコレータを使って作成されたメソッド内で値を算出したら、filter (これから定義します)を通過したものがあった場合のみ必要に応じて処理を起動(lazy load) してstate を変更、画面描写することができるようになっています。

このような処理を行うのにobserver は非常に相性が良いです。

ではfilterdTodos 内に文字をfilter する処理を追加していきましょう。


src/js/TodoStore.js

 import { computed, observable } from "mobx";

class TodoStore {
@observable todos = ["buy milk", "buy eggs"];
@observable filter = "";
@computed get filteredTodos() {
- // do stuff...
+ var matchesFilter = new RegExp(this.filter, "i");
+ return this.todos.filter(todo => !this.filter || matchesFilter.test(todo));
}
}

var store = window.store = new TodoStore();

export default store;


次にTodoList.js ファイルを編集し、filteredTodos を埋め込みます。

filteredTodos も同様に配列形式でtweet リストを取得できるので、map で回すことができます。


src/js/TodoList.js

 import React from "react";

import { observer } from "mobx-react";

@observer
export default class TodoList extends React.Component {
filter(e) {
this.props.store.filter = e.target.value;
}

render() {
- const { filter, todos } = this.props.store;
+ const { filter, filteredTodos, todos } = this.props.store;

- const todoList = todos.map(todo => (
+ const todoList = filteredTodos.map(todo => (
<li>{todo}</li>
));
return <div>
<h1>todos</h1>
<div>{filter}</div>
<input class="filter" value={filter} onChange={this.filter.bind(this)} />
<ul>{todoList}</ul>
</div>;
};
}


次はTodoList.js を編集します。新規tweet を入力するフォームを追加し、ついでにfilter を画面に表示する処理はもう不要なのでこの時点で削除しておきます。

フォームではReturn キーを押下するとstore にフォームの値を渡し、その直後にフォームの内容を空文字でリセットするようにします。


src/js/TodoList.js

 import React from "react";

import { observer } from "mobx-react";

@observer
export default class TodoList extends React.Component {
+ createNew(e) {
+ if (e.which === 13) {
+ this.props.store.createTodo(e.target.value);
+ e.target.value = "";
+ }
+ }
filter(e) {
this.props.store.filter = e.target.value;
}

render() {
const { filter, filteredTodos, todos } = this.props.store;

const todoList = filteredTodos.map(todo => (
<li>{todo}</li>
));
return <div>
<h1>todos</h1>
- <div>{filter}</div>
+ <input class="create" onKeyPress={this.createNew.bind(this)} />
<input class="filter" value={filter} onChange={this.filter.bind(this)} />
<ul>{todoList}</ul>
</div>;
};
}


この時点ではsrc/js/TodoStore.js にcreateTodo 関数が無いので作成します。


src/js/TodoStore.js

 import { computed, observable } from "mobx";

class TodoStore {
@observable todos = ["buy milk", "buy eggs"];
@observable filter = "";
@computed get filteredTodos() {
var matchesFilter = new RegExp(this.filter, "i");
return this.todos.filter(todo => !this.filter || matchesFilter.test(todo));
}
+ createTodo(value) {
+ this.todos.push(value);
+ }
}

var store = window.store = new TodoStore();

export default store;


できました。

ブラウザの画面を開き、動きを確認してみましょう。

左のフォームに文字を入れてReturn を押すと新しいtweet が追加されていきます。

また、右側のフォームに文字を入れるとfilter がリアルタイムに更新され、画面に表示されるtweet がフィルタされて表示されるようになります。

React_ReactMobXApp0004.gif


nested obserbable について

Todo リストに編集する機能を追加することを想定して、それぞれのTodo に対してチェックボックスを追加してみましょう。

それぞれのTodo リストに対してチェックボックスを追加する場合、nested obserbable が利用できます。

チェックボックスをつけるということは、それぞれの配列の要素であるtweet に対してchecked かどうかのstate とid を追加する必要があります。

そういった情報をまとめて管理するためにTodo クラスを作成しましょう。


src/js/TodoStore.js

 import { computed, observable } from "mobx";

+class Todo {
+ @observable value
+ @observable id
+ @observable complete
+
+ constructor(value) {
+ this.value = value;
+ this.id = Date.now();
+ this.complete = false;
+ }
+}
+
class TodoStore {
@observable todos = ["buy milk", "buy eggs"];
@observable filter = "";
@computed get filteredTodos() {
var matchesFilter = new RegExp(this.filter, "i");
return this.todos.filter(todo => !this.filter || matchesFilter.test(todo));
}
createTodo(value) {
this.todos.push(value);
}
}

var store = window.store = new TodoStore();

export default store;


Todo オブジェクトを作成したらtodos にオブジェクトを入れるようにコードも修正します。


src/js/TodoStore.js

 import { computed, observable } from "mobx";

class Todo {
@observable value
@observable id
@observable complete

constructor(value) {
this.value = value;
this.id = Date.now();
this.complete = false;
}
}

class TodoStore {
- @observable todos = ["buy milk", "buy eggs"];
+ @observable todos = [];
@observable filter = "";
@computed get filteredTodos() {
var matchesFilter = new RegExp(this.filter, "i");
return this.todos.filter(todo => !this.filter || matchesFilter.test(todo));
}
createTodo(value) {
- this.todos.push(value);
+ this.todos.push(new Todo(value));
}
}

var store = window.store = new TodoStore();

export default store;


またsrc/js/TodoList.js にTodo オブジェクトのvalue を表示するように書式を変更します。


src/js/TodoList.js

 import React from "react";

import { observer } from "mobx-react";

@observer
export default class TodoList extends React.Component {
createNew(e) {
if (e.which === 13) {
this.props.store.createTodo(e.target.value);
e.target.value = "";
}
}
filter(e) {
this.props.store.filter = e.target.value;
}

render() {
const { filter, filteredTodos, todos } = this.props.store;

const todoList = filteredTodos.map(todo => (
- <li>{todo}</li>
+ <li key={todo.id}>{todo.value}</li>
));
return <div>
<h1>todos</h1>
<input class="create" onKeyPress={this.createNew.bind(this)} />
<input class="filter" value={filter} onChange={this.filter.bind(this)} />
<ul>{todoList}</ul>
</div>;
};
}


この時点で、画面を確認すれば今までどおりtweet が画面から登録できるようになっています。

次にチェックボックスを追加していきましょう。


src/js/TodoList.js

 import React from "react";

import { observer } from "mobx-react";

@observer
export default class TodoList extends React.Component {
createNew(e) {
if (e.which === 13) {
this.props.store.createTodo(e.target.value);
e.target.value = "";
}
}
filter(e) {
this.props.store.filter = e.target.value;
}

render() {
const { filter, filteredTodos, todos } = this.props.store;

const todoList = filteredTodos.map(todo => (
- <li>{todo}</li>
+ <li key={todo.id}>
+ <input type="checkbox" value={todo.complete} checked={todo.complete} />{todo.value}
+ </li>
));
return <div>
<h1>todos</h1>
<input class="create" onKeyPress={this.createNew.bind(this)} />
<input class="filter" value={filter} onChange={this.filter.bind(this)} />
<ul>{todoList}</ul>
</div>;
};
}


これでチェックボックスが表示されるようになりました。

React_ReactMobXApp0005.gif

チェックの状態はTodo オブジェクトのchecked で管理されていますのでTodo オブジェクトのconstructor のthis.complete を修正すればチェックがついた状態でtweet が作成されるようになります。


src/js/TodoStore.js

import { computed, observable } from "mobx";


class Todo {
@observable value
@observable id
@observable complete

constructor(value) {
this.value = value;
this.id = Date.now();
- this.complete = false;
+ this.complete = true;
}
}

...


React_ReactMobXApp0006.gif

(動作確認したら、状態をもとに戻しておきます)


src/js/TodoStore.js

import { computed, observable } from "mobx";


class Todo {
@observable value
@observable id
@observable complete

constructor(value) {
this.value = value;
this.id = Date.now();
- this.complete = true;
+ this.complete = false;
}
}
...


チェックがつくことは確認できたものの、このままでは画面からチェックボックスをクリックしてチェック状態を変更することができません。

React_ReactMobXApp0006_02.gif

クリックしてチェック状態を変更できるようにチェックボックスにonChange を追加し、チェック状態を変更する処理を追加していきましょう。


src/js/TodoList.js

 import React from "react";

import { observer } from "mobx-react";

@observer
export default class TodoList extends React.Component {
createNew(e) {
if (e.which === 13) {
this.props.store.createTodo(e.target.value);
e.target.value = "";
}
}
filter(e) {
this.props.store.filter = e.target.value;
}
+ toggleComplete(todo) {
+ todo.complete = !todo.complete;
+ }

render() {
const { filter, filteredTodos, todos } = this.props.store;

const todoList = filteredTodos.map(todo => (
- <li key={todo.id}><input type="checkbox" />{todo.value}</li>
+ <li key={todo.id}>
+ <input type="checkbox" onChange={this.toggleComplete.bind(this, todo)} value={todo.complete} checked={todo.complete} />{todo.value}
+ </li>
));
return <div>
<h1>todos</h1>
<input class="create" onKeyPress={this.createNew.bind(this)} />
<input class="filter" value={filter} onChange={this.filter.bind(this)} />
<ul>{todoList}</ul>
</div>;
};
}


他の書き方として各チェックボックスにid 属性を付与して、onChange の関数に対してid を渡し、そのid を使ってチェックボックス要素を反転させるやり方もあります。

が、上記のように要素自体を関数の引数として渡すやり方もあります。

処理を追加したらチェックボックスをクリックして、チェックが反転することを確認してみましょう。

React_ReactMobXApp0007.gif

OK です。

次はチェックしたtweet を削除する機能について実装していきましょう。

まずはtodo を削除するためのアンカー(リンク)を追加します。


src/js/TodoList.js

 import React from "react";

import { observer } from "mobx-react";

@observer
export default class TodoList extends React.Component {
createNew(e) {
if (e.which === 13) {
this.props.store.createTodo(e.target.value);
e.target.value = "";
}
}
filter(e) {
this.props.store.filter = e.target.value;
}
toggleComplete(todo) {
todo.complete = !todo.complete;
}

render() {
- const { filter, filteredTodos, todos } = this.props.store;
+ const { clearComplete, filter, filteredTodos, todos } = this.props.store;

const todoList = filteredTodos.map(todo => (
<li key={todo.id}>
<input type="checkbox" onChange={this.toggleComplete.bind(this, todo)} value={todo.complete} checked={todo.complete} />{todo.value}
</li>
));
return <div>
<h1>todos</h1>
<input class="create" onKeyPress={this.createNew.bind(this)} />
<input class="filter" value={filter} onChange={this.filter.bind(this)} />
<ul>{todoList}</ul>
+ <a href="#" onClick={clearComplete}>Clear Complete</a>
</div>;
};
}


onClick の関数ですが、this.props.store.clearComplete としてTodoList.js 内の関数を呼び出すこともできますが、下記のようにTodoStore.js 内に関数を追加することもできます。

今回はTodoStore.js に関数を追加していく形で実装していきたいと思います。


src/js/TodoStore.js

 import { computed, observable } from "mobx";

class Todo {
@observable value
@observable id
@observable complete

constructor(value) {
this.value = value;
this.id = Date.now();
this.complete = false;
}
}

class TodoStore {
@observable todos = [];
@observable filter = "";
@computed get filteredTodos() {
var matchesFilter = new RegExp(this.filter, "i");
- return this.todos.filter(todo => !this.filter || matchesFilter.test(todo));
+ return this.todos.filter(todo => !this.filter || matchesFilter.test(todo.value));
}
createTodo(value) {
this.todos.push(new Todo(value));
+ }
+ clearComplete = () => {
+ const incompleteTodos = this.todos.filter(todo => !todo.complete);
+ this.todos.replace(incompleteTodos);
}
}

var store = window.store = new TodoStore();

export default store;


ここで注意してほしいのは、前のRedux の記事で説明はしましたが、Redux と同様にtodos (state) を変更する時はimmutable を意識して新しいArray オブジェクトを作成して値を変更するようにしてください。

ここではreplace() を使って新しいArray オブジェクトを作成するようにしています。

またtodo をオブジェクト化したことによって正しくfilter が機能しなくなってしまっているのでfilteredTodos メソッド内のmatchesFilter に渡す引数もtodo (オブジェクト)から、todo.value (String)に変更しています。

それではWeb ブラウザから実際にtodo が削除できるかを確認してみましょう。

React_ReactMobXApp0008.gif

動きました!これでTodo アプリの完成です!


まとめ

このように状態管理にMobX を使うことで少ないコードで高反応性のあるアプリケーションを簡単に作成することができます。

機能がシンプルなアプリケーションを作成するにはコード量が少ないMobX を採用する方が可読性の高いコード且つ短時間でアプリケーショを作成できるようになるでしょう。


参考