アクションを使用した状態の更新
使い方
action
(アノテーション)action(fn)
action(name, fn)
@action
(メソッド/フィールドデコレータ)
すべてのアプリケーションにはアクションがあります。アクションとは、状態を修正するコードのことです。原則として、アクションは常にイベントに応じて発生します。たとえば、ボタンがクリックされた、何らかの入力が変更された、websocketメッセージが到着したなどです。
MobXでは、アクションを宣言する必要がありますが、makeAutoObservable
を使用すると、この作業の多くを自動化できます。アクションはコードをより構造化するのに役立ち、次のパフォーマンス上の利点があります。
それらはトランザクション内で実行されます。一番外側のアクションが完了するまで、リアクションは実行されません。これにより、アクション中に生成された中間値または不完全な値は、アクションが完了するまでアプリケーションの他の部分から見えなくなることが保証されます。
デフォルトでは、アクションの外部で状態を変更することは許可されていません。これにより、コードベース内で状態の更新が発生する場所を明確に特定できます。
action
アノテーションは、状態を変更することを目的とする関数でのみ使用する必要があります。情報(ルックアップやデータのフィルタリングを実行)を導出する関数は、MobXがその呼び出しを追跡できるように、アクションとしてマークすべきではありません。action
アノテーションが付けられたメンバは、列挙不可になります。
例
import { makeObservable, observable, action } from "mobx"
class Doubler {
value = 0
constructor() {
makeObservable(this, {
value: observable,
increment: action
})
}
increment() {
// Intermediate states will not become visible to observers.
this.value++
this.value++
}
}
import { observable, action } from "mobx"
class Doubler {
@observable accessor value = 0
@action
increment() {
// Intermediate states will not become visible to observers.
this.value++
this.value++
}
}
import { makeAutoObservable } from "mobx"
class Doubler {
value = 0
constructor() {
makeAutoObservable(this)
}
increment() {
this.value++
this.value++
}
}
import { makeObservable, observable, action } from "mobx"
class Doubler {
value = 0
constructor() {
makeObservable(this, {
value: observable,
increment: action.bound
})
}
increment() {
this.value++
this.value++
}
}
const doubler = new Doubler()
// Calling increment this way is safe as it is already bound.
setInterval(doubler.increment, 1000)
import { observable, action } from "mobx"
const state = observable({ value: 0 })
const increment = action(state => {
state.value++
state.value++
})
increment(state)
import { observable, runInAction } from "mobx"
const state = observable({ value: 0 })
runInAction(() => {
state.value++
state.value++
})
action
を使用した関数のラップ
MobXのトランザクションの性質を最大限に活用するには、アクションをできるだけ外側に渡す必要があります。クラスメソッドが状態を変更する場合は、アクションとしてマークすることをお勧めします。イベントハンドラーをアクションとしてマークする方がさらに優れています。これは、一番外側のトランザクションがカウントされるためです。2つのアクションを連続して呼び出す、マークされていない単一のイベントハンドラーは、それでも2つのトランザクションを生成します。
アクションベースのイベントハンドラーを作成するのに役立つように、action
はアノテーションであるだけでなく、高階関数でもあります。引数として関数を指定して呼び出すことができ、その場合、同じ署名を持つaction
でラップされた関数を返します。
たとえば、Reactでは、onClick
ハンドラーを次のようにラップできます。
const ResetButton = ({ formState }) => (
<button
onClick={action(e => {
formState.resetPendingUploads()
formState.resetValues()
e.preventDefault()
})}
>
Reset form
</button>
)
デバッグの目的で、ラップされた関数に名前を付けるか、action
の最初の引数として名前を渡すことをお勧めします。
注: アクションは追跡されません
アクションのもう1つの機能は、それらが追跡されないことです。副作用または算出値(非常にまれです!)の内部からアクションが呼び出された場合、アクションによって読み取られた観測値は、導出の依存関係としてカウントされません
makeAutoObservable
、extendObservable
、およびobservable
は、autoAction
と呼ばれる特殊なアクションを使用します。これは、関数が導出であるかアクションであるかをランタイム時に判断します。
action.bound
使い方
action.bound
(アノテーション)
action.bound
アノテーションを使用すると、メソッドを正しいインスタンスに自動的にバインドして、関数内でthis
が常に正しくバインドされるようにできます。
ヒント: すべてのアクションとフローを自動的にバインドするには、makeAutoObservable(o, {}, { autoBind: true })
を使用してください
import { makeAutoObservable } from "mobx"
class Doubler {
value = 0
constructor() {
makeAutoObservable(this, {}, { autoBind: true })
}
increment() {
this.value++
this.value++
}
*flow() {
const response = yield fetch("http://example.com/value")
this.value = yield response.json()
}
}
runInAction
使い方
runInAction(fn)
このユーティリティを使用して、すぐに呼び出される一時的なアクションを作成します。非同期プロセスで役立つ可能性があります。例については、上記のコードブロックを参照してください。
アクションと継承
プロトタイプで定義されたアクションのみが、サブクラスによってオーバーライドできます。
class Parent {
// on instance
arrowAction = () => {}
// on prototype
action() {}
boundAction() {}
constructor() {
makeObservable(this, {
arrowAction: action
action: action,
boundAction: action.bound,
})
}
}
class Child extends Parent {
// THROWS: TypeError: Cannot redefine property: arrowAction
arrowAction = () => {}
// OK
action() {}
boundAction() {}
constructor() {
super()
makeObservable(this, {
arrowAction: override,
action: override,
boundAction: override,
})
}
}
単一のアクションをthis
にバインドするには、アロー関数の代わりにaction.bound
を使用できます。
詳細については、サブクラス化を参照してください。
非同期アクション
本質的に、非同期プロセスは、それらが引き起こされる時間に関係なく、すべてのリアクションが自動的に更新されるため、MobXで特別な処理は必要ありません。また、観測可能なオブジェクトは変更可能であるため、アクションの期間中、それらへの参照を保持することは一般的に安全です。ただし、非同期プロセスで観測値を更新するすべてのステップ(ティック)は、action
としてマークする必要があります。これは、以下に示すように、上記のAPIを活用することで複数の方法で実現できます。
たとえば、promiseを処理する場合、状態を更新するハンドラーはアクションであるか、以下に示すようにaction
を使用してラップする必要があります。
Promise解決ハンドラーはインラインで処理されますが、元のアクションが完了した後に実行されるため、action
でラップする必要があります
import { action, makeAutoObservable } from "mobx"
class Store {
githubProjects = []
state = "pending" // "pending", "done" or "error"
constructor() {
makeAutoObservable(this)
}
fetchProjects() {
this.githubProjects = []
this.state = "pending"
fetchGithubProjectsSomehow().then(
action("fetchSuccess", projects => {
const filteredProjects = somePreprocessing(projects)
this.githubProjects = filteredProjects
this.state = "done"
}),
action("fetchError", error => {
this.state = "error"
})
)
}
}
promiseハンドラーがクラスフィールドの場合、makeAutoObservable
によって自動的にaction
でラップされます
import { makeAutoObservable } from "mobx"
class Store {
githubProjects = []
state = "pending" // "pending", "done" or "error"
constructor() {
makeAutoObservable(this)
}
fetchProjects() {
this.githubProjects = []
this.state = "pending"
fetchGithubProjectsSomehow().then(this.projectsFetchSuccess, this.projectsFetchFailure)
}
projectsFetchSuccess = projects => {
const filteredProjects = somePreprocessing(projects)
this.githubProjects = filteredProjects
this.state = "done"
}
projectsFetchFailure = error => {
this.state = "error"
}
}
await
後のすべてのステップは同じティック内ではないため、アクションのラッピングが必要です。ここでは、runInAction
を活用できます
import { runInAction, makeAutoObservable } from "mobx"
class Store {
githubProjects = []
state = "pending" // "pending", "done" or "error"
constructor() {
makeAutoObservable(this)
}
async fetchProjects() {
this.githubProjects = []
this.state = "pending"
try {
const projects = await fetchGithubProjectsSomehow()
const filteredProjects = somePreprocessing(projects)
runInAction(() => {
this.githubProjects = filteredProjects
this.state = "done"
})
} catch (e) {
runInAction(() => {
this.state = "error"
})
}
}
}
import { flow, makeAutoObservable, flowResult } from "mobx"
class Store {
githubProjects = []
state = "pending"
constructor() {
makeAutoObservable(this, {
fetchProjects: flow
})
}
// Note the star, this a generator function!
*fetchProjects() {
this.githubProjects = []
this.state = "pending"
try {
// Yield instead of await.
const projects = yield fetchGithubProjectsSomehow()
const filteredProjects = somePreprocessing(projects)
this.state = "done"
this.githubProjects = filteredProjects
return projects
} catch (error) {
this.state = "error"
}
}
}
const store = new Store()
const projects = await flowResult(store.fetchProjects())
async / awaitの代わりにflowを使用する {🚀}
使い方
flow
(アノテーション)flow(function* (args) { })
@flow
(メソッドデコレータ)
flow
ラッパーは、MobXアクションをより簡単に使用できるようにするasync
/ await
のオプションの代替手段です。flow
は、ジェネレーター関数を唯一の入力として受け取ります。ジェネレーター内では、promiseをyieldすることによって連結できます(await somePromise
の代わりに、yield somePromise
と記述します)。その後、flowメカニズムは、yieldされたpromiseが解決されたときに、ジェネレーターが継続するかスローするかを保証します。
したがって、flow
は、これ以上のaction
ラッピングを必要としないasync
/ await
の代替手段です。これは次のように適用できます
- 非同期関数の周りに
flow
をラップします。 async
の代わりにfunction *
を使用してください。await
の代わりにyield
を使用してください。
上記のflow
+ ジェネレーター関数の例は、実際にどのようなものかを示しています。
flowResult
関数は、TypeScript を使用する場合にのみ必要となることに注意してください。メソッドを flow
でデコレートすると、返されたジェネレーターが Promise でラップされます。ただし、TypeScript はその変換を認識しないため、flowResult
は TypeScript がその型の変更を認識するようにします。
makeAutoObservable
などは、ジェネレーターを自動的に flow
として推論します。flow
で注釈されたメンバーは、列挙不可能になります。
{🚀} 注意: オブジェクトフィールドで flow を使用する
flow
は、action
と同様に、関数を直接ラップするために使用できます。上記の例は、次のように記述することもできます。
import { flow, makeObservable, observable } from "mobx"
class Store {
githubProjects = []
state = "pending"
constructor() {
makeObservable(this, {
githubProjects: observable,
state: observable,
})
}
fetchProjects = flow(function* (this: Store) {
this.githubProjects = []
this.state = "pending"
try {
// yield instead of await.
const projects = yield fetchGithubProjectsSomehow()
const filteredProjects = somePreprocessing(projects)
this.state = "done"
this.githubProjects = filteredProjects
} catch (error) {
this.state = "error"
}
})
}
const store = new Store()
const projects = await store.fetchProjects()
利点は、flowResult
が不要になったことですが、欠点は、その型が正しく推論されるように this
の型を指定する必要があることです。
flow.bound
使い方
flow.bound
(アノテーション)
flow.bound
アノテーションを使用すると、メソッドを正しいインスタンスに自動的にバインドできるため、関数内で常に this
が正しくバインドされます。アクションと同様に、フローはautoBind
オプションを使用してデフォルトでバインドできます。
フローのキャンセル {🚀}
フローのもう1つの優れた点は、キャンセル可能なことです。flow
の戻り値は、最終的にジェネレーター関数から返される値で解決される Promise です。返された Promise には、実行中のジェネレーターを中断してキャンセルする追加の cancel()
メソッドがあります。try
/ finally
句は、引き続き実行されます。
必須アクションの無効化 {🚀}
デフォルトでは、MobX 6 以降では、状態を変更するためにアクションを使用する必要があります。ただし、この動作を無効にするように MobX を構成できます。enforceActions
セクションを確認してください。たとえば、これはユニットテストの設定で非常に役立つ可能性があります。警告には必ずしも多くの価値があるとは限りません。