Open hysryt opened 2 years ago
React用のステート管理ライブラリ。 同じような用途のライブラリにはRecoilやReduxがある。
関数型リアクティブプログラミング(Functional Reactive Programming)
npx create-react-app my-app
cd my-app
npm install mobx mobx-react
MobX は ES5 の環境で動作する。 Reactと一緒に使う場合は mobx-react または mobx-react-lite が必要。 lite の方はクラスコンポーネントをサポートしていない。
3つの概念が重要
import { makeObservable, observable, action } from "mobx"
class Todo {
id = Math.random()
title = ""
finished = false
constructor(title) {
makeObservable(this, {
title: observable,
finished: observable,
toggle: action
})
this.title = title
}
toggle() {
this.finished = !this.finished
}
}
MobXはプロパティ、オブジェクト、配列など、さまざまなものを監視(observe)対象とすることができる。
監視対象とするには makeObservable()
や makeAutoObservable()
、observable()
を使用する。
class CounterState {
value = 0;
constructor() {
makeObservable(this, {
value: observable,
increment: action,
});
}
increment() {
this.value++;
}
}
makeObservable()
の第二引数はアノテーションと呼び、監視を行う上でのプロパティやメソッドの役割を記述する。
役割の中でも重要なものが以下の3つ。
observable
- 監視対象。ステート。action
- ステートの値を変更するメソッド。computed
- ステートをもとに新しい値を返すゲッターメソッド。返り値はキャッシュされる。ゲッターメソッドであっても、ステートだけでなく引数にも依存するメソッドの場合はアノテーションは不要で、キャッシュはされないらしい。(メモリリーク対策とのこと)
class CounterState {
value = 0;
constructor() {
makeAutoObservable(this);
}
increment() {
this.value++;
}
}
makeObservable()
の強化版。自動で全てのプロパティを推測する。
アノテーションを変更したい場合は第二引数で上書きできる。
サブクラスでは使用できない。
アノテーションのルールは以下の通り
observable
get
が指定された関数は computed
set
が指定された関数は action
autoAction
flow
const todosById = observable({
"TODO-123": {
title: "find a decent task management system",
done: false
}
})
第一変数を監視する。 Proxyを使用しているため、後からオブジェクトに追加したものも監視の対象となる。
action はトランザクション内で実行される。 action の実行が完了するまで途中の値は外からは見えない。
strictモードが有効な場合、action以外からステートの値を変更することはできない。
メソッドを action としてアノテーションするには makeObservable()
、 makeAutoObservable()
を使う。
observable()
で作成したステートを変更する action を作成するには action()
、runInAction()
を使う。
const state = observable({ value: 0 });
const increment = action(state => {
state.value++
});
action は関数なので action のなかで action を実行するということもある。 その場合、一番外側の action がトランザクションとなる。
イベントハンドラの中で複数のステート変更を行う場合、イベントハンドラ自体を action とすると良い。
const ResetButton = ({ formState }) => (
<button
onClick={action(e => {
formState.resetPendingUploads()
formState.resetValues()
e.preventDefault()
})}
>
Reset form
</button>
)
const state = observable({ value: 0 });
runInAction(() => {
state.value++
});
すぐに起動される一時的なアクションを作成する。
mobx/src/types/observableobject.ts:614
ObservableObjectAdministration
を生成し、target
の隠しプロパティに追加する。
target
を返す。
observable
アノテーションは Proxy を使ってるっぽい
/mobx/src/types/observableobject.ts:350
observable()
が使えるのはプリミティブ型、プレーンオブジェクト、配列、Map、Setのみ。
クラスのインスタンスには使えない。
内部的には createObservable()
を呼び出し、ターゲットの型に応じて
observable.box()
- プリミティブ型observable.object()
- プレーンオブジェクトobservable.array()
- 配列observable.map()
- Mapobservable.set()
- Setに振り分ける。
ObservableValue
を返す。
const state = mobx.observable.box(0);
mobx.autorun(() => {
console.log(state.get());
});
setInterval(() => {
state.set(state.get() + 1);
}, 1000);
値には get()
と set()
でアクセスする。
let state = mobx.observable.object({
counter: 0,
});
mobx.autorun(() => {
console.log(state.counter);
});
setInterval(() => {
state.counter++;
}, 1000);
与えたオブジェクトと同じプロパティを持つオブジェクトを返す(Proxyを使える環境ではProxy)。
Proxy を使える環境では asDynamicObservableObject()
、使えない環境では asObservableObject()
を呼び出して ObservableObject を生成したのち、extendObservable()
でプロパティを付与する。
仕組みとしては与えられたオブジェクトのプロパティ名を取得し、そのプロパティ名のsetter/getterを持つオブジェクトを新たに作ってるっぽい。
Proxyが使える環境では新たに作成したオブジェクトをProxyでラップする。Proxyによって、後からプロパティを追加できたりするようになる。
Proxyのハンドラは src/types/dynamicobject.ts:20
あたり。
getter
を呼び出したときは reportObserved()
を実行している。(依存関係を記録するためっぽい)
$mobx
プロパティを持つプレーンオブジェクトを返す。( $mobx
はシンボル)
target
を IIsObservableObject
として取得する。
target
が $mobx
プロパティを持たない場合、 ObservableObjectAdministration
をインスタンス化して target[$mobx]
に格納する。
if (proxyTrap) {
if (!Reflect.defineProperty(this.target_, key, descriptor)) {
return false
}
} else {
defineProperty(this.target_, key, descriptor)
}
Reflect.defineProperty()
は boolean
を返すのに対し、Object.defineProperty()
はオブジェクトを返す。
失敗時は Reflect.defineProperty()
は false
を返し、Object.defineProperty()
は TypeError
を投げる。
監視対象のオブジェクトの $mobx
プロパティに入っている。
ObservableValue を管理している?
実際にデータを持っているのはオブジェクトではなくこのインスタンス。
第一引数で渡す関数は引数としてReactionのインスタンスを取得する。
Reaction
をインスタンス化Reactionは特別なderivationsである。通常のリアクティブ計算とは異なる点がいくつかあります。
リアクションのステートマシンは以下の通りです。
runReaction
を呼び出すか、スケジューリングしてリアクションを開始します (autorun
も参照してください)。onInvalidate
ハンドラは、何らかの方法でthis.track (someFunction)
を呼び出します。someFunction
からアクセスされるすべての観測変数は、この反応によって観測されます。onInvalidate
が呼び出され、手順1に戻ります。schedule_()
で globalState.pendingReactions
に追加される。
その後グローバルの runReactions()
が実行される。
runReactions()
は結果的に runReactionsHelper()
を呼び出す。
globalState.isRunningReactions
を true
にする。
globalState.pendingReactions
の各要素で runReaction_()
を実行する。
globalState.isRunningReactions
を false
にする。
Reactionインスタンスの関数。
globalState.trackingContext
に自分を格納する。
https://medium.com/hackernoon/becoming-fully-reactive-an-in-depth-explanation-of-mobservable-55995262a254#.xvbh6qd74 https://hackernoon.com/the-fundamental-principles-behind-mobx-7a725f71f3e8
export interface IDerivation extends IDepTreeNode {
observing_: IObservable[]
newObserving_: null | IObservable[]
dependenciesState_: IDerivationState_
runId_: number
unboundDepsCount_: number
onBecomeStale_(): void
isTracing_: TraceMode
requiresObservable_?: boolean
}
flow
を使うflow
ラッパーは、async/awaitに代わるオプションで、MobXのアクションをより簡単に操作できるようにするものです。flow
は、ジェネレータ関数を唯一の引数とします。ジェネレータの内部では,Promiseをyieldすることでチェインさせることができます(await somePromiseの代わりにyield somePromiseと記述します)。そして、flow のメカニズムは、yield された Promise が解決されたときに、ジェネレータが継続するか、投げるかを確認します。つまり、flowはasync / awaitに代わる、さらなるアクションラッピングを必要としないものなのです。以下のように適用することができます。
なお、flowResult関数はTypeScriptを使用する場合のみ必要です。メソッドを flow でデコレートするため、返されるジェネレータをプロミスでラップすることになります。しかし、TypeScriptはその変換を意識していないので、flowResultはTypeScriptがその型の変更を意識していることを確認することになります。
makeAutoObservableは、自動的にジェネレータをflowに推論します。flowアノテーションのメンバーは非列挙型になります。
https://mobx.js.org/README.html