MSTの概要
ポストReduxとして注目を浴びているのが、Mobxの拡張であるMST(Mobx-state-tree)です。Mobxはシンプルで扱いやすい一方で、いろいろな問題が起きやすいため、タイプ情報を定義することによって、それらを極めてラディカルに解決しようというのがこのMSTになります。
今回はMobX-State-Treeの基本的な仕組みと書き方をメモします。
MSTとは何か
Mobx単体との違い
主な参考記事:
https://medium.com/react-native-training/state-management-with-mobx-state-tree-373f9f2dc68a
何が違うか:
- Opinionated(独善的): データがどのように構造化されるべきか、または更新されるべきかがルール化されている。
- Typed — 独自のtypes APIがある
- Models — 複数モデルのツリーが生成される
observable(観察可能)であること、mutable(高可変性)であることは同じです。
哲学
公式サイトより翻訳(意訳あり)
https://mobx-state-tree.js.org/intro/philosophy
**MST(mobx-state-tree)は、 可変データの「シンプルさ」と「扱いやすさ」、不変データの「Reactivity」、「トレーサビリティ」、そして「観測可能データのパフォーマンス」を備えたStateのコンテナ(container)**です。
簡単にいうと、MSTは、状態管理に対する不変性(取引のしやすさ、トレーサビリティ、構成)と可変性(検出のしやすさ、配列法、カプセル化)の両方のアプローチの最良の特徴を組み合わせることを目的としています。
Mobx単体とは違って、MSTはデータをどのように構造化し、更新すべきかを独自のルールに従って決めることで、Mobxでよく起こる問題をすぐに解決できます。
MSTの中心にあるのは、**生きた木(ツリー)という概念です。このツリーは、変更可能ですが厳密に保護されたオブジェクトで構成され、実行時タイプ情報が付加されます。つまり、各ツリーには形状(タイプ情報)と状態(データ)**があります。
この生きた木から、普遍的な、共通のスナップショットが自動的に生成されます。
サンプルコード
以下は、公式サイトにある簡易的なサンプルコードを少し修正したものです。
import { types, onSnapshot } from "mobx-state-tree"
// typesを使ってmodel構造とactionsを定義
const Todo = types
.model("Todo", {
title: types.string,
subTitle: types.string,
done: false
})
.views(self => ({
fullTitle() {
return self.title + self.subTitle
}
}))
.actions(self => ({
toggle() {
self.done = !self.done
}
}))
// StoreでTodoモデルをラップ
const Store = types.model("Store", {
todos: types.array(Todo)
})
// Storeからインスタンスを作成
const store = Store.create({
todos: [
{
title: "Get coffee"
}
]
})
// 新しいsnapshotの作成をlisten
onSnapshot(store, snapshot => {
console.dir(snapshot)
})
// toggleアクションを呼んでみる
store.todos[0].toggle()
// doneのtrue/falseが切り替わる
// onSnapshotが発火し、コンソールログに`{ todos: [{ title: "Get coffee", done: true }]}`が表示される
// viewsからフルタイトルを取得する
store.todos[0].title()
modelファイルはmodels/配下に置くのが一般的なようです。
Reduxに比べてめちゃくちゃシンプルですが、グローバルなState/Action管理という目的では特に過不足ないかと思います。
Example
公式でExampleが用意されています。
オブジェクトのネストと配列
例えば、以下のようなネストされたオブジェクトや配列を管理したい場合について解説します。
{
"id": 1,
"location": {
"latitude": 135.22313,
"longitude": 41.2335,
},
"tags": [{name: "Male"}, {name: "Premium"},{name: "Approved"}],
"addresses": [
{
"id": 2,
"zip_code": "1234567",
"prefectures": "東京都",
"city": "渋谷区",
"street_address": "渋谷",
"street_address_optional": "1-23-4",
}
]
}
オブジェクトの形式のデータをネストする場合は、modelを作成してそれをネストします。
const locationModel = types.model({
latitude: types.number,
selected: types.number
});
const userModel = types.model({
id: 1,
location: locationModel,
...
});
配列の場合は、types.arrayを使用します。
const tagModel = types.model({
name: types.string
});
const userModel = types.model({
id: 1,
tags: types.array(tagModel),
...
});
Viewとの組み合わせ方
observeはmobx-reactで提供されているモジュールを使います。
import { observer } from "mobx-react";
const App = observer(props => (
<div>
<button onClick={e => props.store.addTodo(randomId(), "New Task")}>Add Task</button>
{values(props.store.todos).map(todo => (
<div>
<input type="checkbox" checked={todo.done} onChange={e => todo.toggle()} />
<input type="text" value={todo.name} onChange={e => todo.setName(e.target.value)} />
</div>
))}
</div>
))
まとめ
自社のReact Nativeアプリで導入しましたが特に大きな問題はなさそうです。
Reduxのように洗練されたベストプラクティスがないため、Storeの設計などには苦労しそうです。