MobX
は、シンプルでスケーラブルかつ実戦でテスト済みの状態管理ソリューションです。このチュートリアルでは、10分間でMobXの重要な概念をすべて学びます。MobXはスタンドアロンライブラリですが、多くの人がReactで使用しており、このチュートリアルではその組み合わせに焦点を当てています。
中心となる考え方
状態は各アプリケーションの中心であり、一貫性のない状態や、周囲に残っているローカル変数と同期していない状態を作り出すことほど、バグが多く、管理が困難なアプリケーションを作成する速い方法はありません。そのため、多くの状態管理ソリューションは、状態を変更できる方法を制限しようとします。たとえば、状態を不変にすることなどです。しかし、これにより新しい問題が発生します。データを正規化する必要があり、参照整合性を保証できなくなり、クラスのような強力な概念を使用することがほぼ不可能になります。
MobXは、根本的な問題に対処することで、状態管理を再びシンプルにします。つまり、一貫性のない状態を作成することを不可能にします。それを達成するための戦略はシンプルです。アプリケーションの状態から導き出せるものはすべて、自動的に導き出されるようにします。
概念的には、MobXはあなたのアプリケーションをスプレッドシートのように扱います。
- まず、アプリケーションの状態があります。オブジェクト、配列、プリミティブ、参照のグラフは、アプリケーションのモデルを形成します。これらの値は、アプリケーションの「データセル」です。
- 次に、導出があります。基本的に、アプリケーションの状態から自動的に計算できる値です。これらの導出値、または計算値は、未完了のToDoの数のような単純な値から、ToDoの視覚的なHTML表現のような複雑なものまで、範囲があります。スプレッドシートの用語では、これらはアプリケーションの式とグラフです。
- リアクションは導出と非常によく似ています。主な違いは、これらの関数は値を生成しないことです。代わりに、タスクを実行するために自動的に実行されます。通常、これはI/O関連です。これらは、DOMが更新されるか、ネットワークリクエストが適切なタイミングで自動的に行われるようにします。
- 最後に、アクションがあります。アクションは、状態を変更するすべてのものです。MobXは、アクションによって引き起こされたアプリケーションの状態へのすべての変更が、すべての導出とリアクションによって自動的に処理されるようにします。同期的に、そしてグリッチフリーで。
シンプルなToDoストア…
理論は十分です。実際に動作しているのを見る方が、上記の内容を注意深く読むよりも多くのことを説明するでしょう。独創性のために、非常にシンプルなToDoストアから始めましょう。以下のすべてのコードブロックは編集可能なので、コードを実行ボタンを使用して実行してください。以下は、ToDoのコレクションを維持する非常に簡単なTodoStore
です。まだMobXは使用していません。
xxxxxxxxxx
class TodoStore {
todos = [];
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}`;
}
addTodo(task) {
this.todos.push({
task: task,
completed: false,
assignee: null
});
}
}
const todoStore = new TodoStore();
todoStore
インスタンスとtodos
コレクションを作成しました。いくつかのオブジェクトでtodoStoreを埋める時間です。変更の効果を確認するために、各変更後にtodoStore.report
を呼び出し、ログを出力します。レポートは意図的に常に最初のタスクのみを出力します。この例を少し人工的にしますが、後でわかるように、MobXの依存関係追跡が動的であることをうまく示しています。
xxxxxxxxxx
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());
リアクティブになる
今のところ、このコードには特別なものは何もありません。しかし、report
を明示的に呼び出す必要がなく、代わりに関連する状態の変更ごとに呼び出されるように宣言できたらどうでしょうか?これにより、コードベースのreport
を呼び出す責任から解放されます。最新のレポートが出力されるようにしたいです。しかし、その整理に悩まされるのは避けたいです。
幸いなことに、それはまさにMobXが私たちのためにできることです。状態のみに依存するコードを自動的に実行します。そのため、report
関数は、スプレッドシートのグラフのように自動的に更新されます。それを達成するために、TodoStore
を観察可能にする必要があります。そうすれば、MobXは行われているすべての変更を追跡できます。それを達成するために、クラスを少し変更する必要があります。
また、completedTodosCount
プロパティは、ToDoリストから自動的に導出できます。observable
とcomputed
アノテーションを使用することにより、オブジェクトに観察可能なプロパティを導入できます。以下の例では、アノテーションを明示的に示すためにmakeObservable
を使用していますが、このプロセスを簡素化するためにmakeAutoObservable(this)
を使用することもできます。
xxxxxxxxxx
class ObservableTodoStore {
todos = [];
pendingRequests = 0;
constructor() {
makeObservable(this, {
todos: observable,
pendingRequests: observable,
completedTodosCount: computed,
report: computed,
addTodo: action,
});
autorun(() => console.log(this.report));
}
get completedTodosCount() {
return this.todos.filter(
todo => todo.completed === true
).length;
}
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}`;
}
addTodo(task) {
this.todos.push({
task: task,
completed: false,
assignee: null
});
}
}
const observableTodoStore = new ObservableTodoStore();
以上です!いくつかのプロパティをobservable
としてマークして、これらの値が時間の経過とともに変化する可能性があることをMobXに知らせました。計算はcomputed
でデコレートされ、これらは状態から導出でき、基礎となる状態が変化しない限りキャッシュされることが識別されます。
pendingRequests
とassignee
属性はまだ使用されていませんが、このチュートリアルの後半で使用されます。
コンストラクタでは、report
を出力する小さな関数を作成し、autorun
でラップしました。Autorunは、1回実行され、その後、関数内で使用された観察可能なデータが変更されるたびに自動的に再実行されるリアクションを作成します。report
は観察可能なtodos
プロパティを使用しているため、適切なタイミングでレポートを出力します。これは次のリストで示されています。実行ボタンを押してください。
xxxxxxxxxx
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";
楽しいですよね?report
は自動的に、同期的に、中間値を漏らすことなく出力されました。ログをよく調べてみると、5行目は新しいログ行になりませんでした。レポートは、基盤となるデータが変化したにもかかわらず、実際には変更されなかったためです。一方、最初のToDoの名前を変更すると、その名前がレポートで積極的に使用されているため、レポートが更新されました。これは、todos
配列だけでなく、ToDoアイテム内の個々のプロパティもautorun
によって観察されていることをうまく示しています。
Reactをリアクティブにする
さて、これまで私たちはばかげたレポートをリアクティブにしました。このまったく同じストアの周りにリアクティブなユーザーインターフェースを作成する時間です。Reactコンポーネントは(その名前にもかかわらず)すぐにリアクティブではありません。mobx-react-lite
パッケージのobserver
HoCラッパーは、基本的にReactコンポーネントをautorun
でラップすることで、それを修正します。これにより、コンポーネントは状態と同期した状態に保たれます。これは概念的には、前にreport
で行ったことと変わりません。
次のリストでは、いくつかのReactコンポーネントを定義しています。そこにあるMobX固有のコードは、observer
ラッピングだけです。関連するデータが変更されたときに、各コンポーネントが個別に再レンダリングされるようにするには、これだけで十分です。状態useState
セッターを呼び出す必要も、セレクターや構成が必要な高階コンポーネントを使用してアプリケーション状態の適切な部分にサブスクライブする方法を考え出す必要もありません。基本的に、すべてのコンポーネントがスマートになりました。それでも、それらはダムで宣言的な方法で定義されています。
コードを実行ボタンを押して、以下のコードを動作させてください。リストは編集可能なので、自由に操作してください。たとえば、すべてのobserver
呼び出し、またはTodoView
をデコレートしている呼び出しを削除してみてください。右側のプレビューの数値は、コンポーネントがレンダリングされた回数を強調表示しています。
xxxxxxxxxx
const TodoList = observer(({store}) => {
const onNewTodo = () => {
store.addTodo(prompt('Enter a new todo:','coffee plz'));
}
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={ onNewTodo }>New Todo</button>
<small> (double-click a todo to edit)</small>
<RenderCounter />
</div>
);
})
const TodoView = observer(({todo}) => {
const onToggleCompleted = () => {
todo.completed = !todo.completed;
}
const onRename = () => {
todo.task = prompt('Task name', todo.task) || todo.task;
}
return (
<li onDoubleClick={ onRename }>
<input
type='checkbox'
checked={ todo.completed }
onChange={ onToggleCompleted }
/>
{ todo.task }
{ todo.assignee
? <small>{ todo.assignee.name }</small>
: null
}
<RenderCounter />
</li>
);
})
ReactDOM.render(
<TodoList store={ observableTodoStore } />,
document.getElementById('reactjs-app')
);
次のリストは、さらなる帳簿管理を行うことなく、データを変更するだけで済むことをうまく示しています。MobXは、ストアの状態からユーザーインターフェースの関連部分を自動的に再導出し、更新します。
xxxxxxxxxx
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 });
// etc etc.. add your own statements here...
参照の使用方法
これまでのところ、観察可能なオブジェクト(プロトタイプとプレーンオブジェクトの両方)、配列、プリミティブを作成しました。MobXでは参照はどのように処理されるのでしょうか?私の状態はグラフを形成することを許可されていますか?前のリストでは、ToDoにassignee
プロパティがあることに気付いたかもしれません。人々を含む別の「ストア」(まあ、それは単なるglorified arrayです)を導入し、彼らにタスクを割り当てることによって、それらにいくつかの値を与えましょう。
xxxxxxxxxx
const peopleStore = observable([
{ name: "Michel" },
{ name: "Me" }
]);
observableTodoStore.todos[0].assignee = peopleStore[0];
observableTodoStore.todos[1].assignee = peopleStore[1];
peopleStore[0].name = "Michel Weststrate";
これで、人々とToDoの2つの独立したストアができました。peopleストアからpersonをassignee
に割り当てるには、参照を割り当てるだけです。これらの変更は、TodoView
によって自動的に取得されます。MobXを使用すると、最初にデータを正規化したり、コンポーネントが更新されるようにセレクターを作成したりする必要はありません。実際、データがどこにあるかは関係ありません。オブジェクトが観察可能になる限り、MobXはそれらを追跡できます。実際のJavaScript参照は機能します。導出に関連する場合は、MobXが自動的にそれらを追跡します。それをテストするには、次の入力ボックスで名前を変更してみてください(上記のコードを実行ボタンを最初に押していることを確認してください!)。
あなたの名前
ちなみに、上記の入力ボックスのHTMLは単純に
<input onkeyup="peopleStore[1].name = event.target.value" />
非同期アクション
小さなToDoアプリケーションのすべてが状態から導出されているため、状態が変更されるタイミングは実際には問題になりません。これにより、非同期アクションの作成が非常に簡単になります。次のボタン(複数回)を押して、非同期的に新しいToDoアイテムを読み込むことをエミュレートします。
その背後にあるコードは非常に簡単です。ストアのプロパティpendingRequests
を更新して、UIに現在の読み込み状態を反映させることから始めます。読み込みが完了したら、ストアのToDoを更新し、pendingRequests
カウンターを再び減らします。このスニペットを前のTodoList
定義と比較して、pendingRequests
プロパティの使用方法を確認してください。
タイムアウト関数はaction
でラップされています。これは厳密には必要ありませんが、両方の変更が単一トランザクションで処理され、オブザーバーが両方の更新が完了した後にのみ通知されるようにします。
observableTodoStore.pendingRequests++; setTimeout(action(() => { observableTodoStore.addTodo('Random Todo ' + Math.random()); observableTodoStore.pendingRequests--; }), 2000);
結論
以上です!定型コードはありません。私たちの完全なUIを形成する、いくつかの単純で宣言的なコンポーネントだけです。そして、ストアの状態から完全にリアクティブに導出されます。これで、独自のアプリケーションでmobx
とmobx-react-lite
パッケージの使用を開始する準備が整いました。これまで学習した内容の簡単な要約です。
- オブジェクトをMobXで追跡可能にするには、
observable
デコレーターまたはobservable(object or array)
関数を使用します。 computed
デコレーターは、状態から値を自動的に導出し、キャッシュできる関数の作成に使用できます。autorun
を使用して、観察可能な状態に依存する関数を自動的に実行します。これは、ログ出力、ネットワークリクエストの実行などに役立ちます。mobx-react-lite
パッケージのobserver
ラッパーを使用して、Reactコンポーネントを真にリアクティブにします。大量のデータを含む大規模で複雑なアプリケーションで使用した場合でも、自動的かつ効率的に更新されます。
上の編集可能なコードブロックで少し長く遊んでみて、MobXがどのように変更に反応するかを基本的な感覚で掴んでください。 例えば、report
関数にログステートメントを追加して、いつ呼び出されるかを確認することができます。あるいは、report
を全く表示せずに、それが TodoList
のレンダリングにどのように影響するかを確認することもできます。特定の状況下でのみ表示することもできます…
MobXはアーキテクチャを規定しません
上記の例は contrived(人工的な)であることにご注意ください。メソッドでロジックをカプセル化したり、ストアやコントローラー、ビューモデルなどで整理するなど、適切なエンジニアリングプラクティスを使用することをお勧めします。多くの異なるアーキテクチャパターンを適用することができ、そのいくつかは公式ドキュメント内でさらに詳しく説明されています。上記の例と公式ドキュメントの例は、MobXをどのように使用できるかを示しており、どのように使用する必要があるかを示しているわけではありません。 あるいは、HackerNewsのある人が言ったように
「MobXは、他の場所でも言及されていますが、賞賛せずにはいられません。MobXで記述するということは、コントローラー/ディスパッチャー/アクション/スーパーバイザー、またはその他のデータフロー管理形式の使用が、Todoアプリ以上のものにはデフォルトで必要とされるものではなく、アプリケーションのニーズに合わせてパターン化できるアーキテクチャ上の懸念事項に戻ることを意味します。」