リアクションによる副作用の実行 {🚀}
リアクションは、MobXの全てが統合される重要な概念です。リアクションの目的は、自動的に発生する副作用をモデル化することです。その重要性は、オブザーバブル状態のコンシューマを作成し、関連する何かが変更されるたびに自動的に副作用を実行することです。
しかし、それを念頭に置いて、ここで説明するAPIはめったに使用されるべきではないことに気づくことが重要です。それらは、多くの場合、他のライブラリ(mobx-reactなど)またはアプリケーション固有の抽象化で抽象化されています。
しかし、MobXを理解するために、リアクションの作成方法を見てみましょう。最も簡単な方法は、autorun
ユーティリティを使用することです。それ以外にも、reaction
とwhen
もあります。
Autorun
使用方法
autorun(effect: (reaction) => void, options?)
autorun
関数は、監視しているものが変更されるたびに実行される関数を受け取ります。また、autorun
自体を作成したときにも一度実行されます。これは、`observable`または`computed`と注釈を付けたオブザーバブル状態の変更のみに反応します。
トラッキングの仕組み
Autorunは、リアクティブコンテキストでeffect
を実行することによって機能します。提供された関数の実行中、MobXは、効果によって直接的または間接的に読み取られるすべてのオブザーバブルとコンピュート値を追跡します。関数が終了すると、MobXは読み取られたすべてのオブザーバブルを収集してサブスクライブし、それらのいずれかが再び変更されるまで待ちます。変更されると、autorun
は再びトリガーされ、プロセス全体が繰り返されます。
これが以下の例がどのように機能するかです。
例
import { makeAutoObservable, autorun } from "mobx"
class Animal {
name
energyLevel
constructor(name) {
this.name = name
this.energyLevel = 100
makeAutoObservable(this)
}
reduceEnergy() {
this.energyLevel -= 10
}
get isHungry() {
return this.energyLevel < 50
}
}
const giraffe = new Animal("Gary")
autorun(() => {
console.log("Energy level:", giraffe.energyLevel)
})
autorun(() => {
if (giraffe.isHungry) {
console.log("Now I'm hungry!")
} else {
console.log("I'm not hungry!")
}
})
console.log("Now let's change state!")
for (let i = 0; i < 10; i++) {
giraffe.reduceEnergy()
}
このコードを実行すると、次の出力が得られます。
Energy level: 100
I'm not hungry!
Now let's change state!
Energy level: 90
Energy level: 80
Energy level: 70
Energy level: 60
Energy level: 50
Energy level: 40
Now I'm hungry!
Energy level: 30
Energy level: 20
Energy level: 10
Energy level: 0
上記の出力の最初の2行に示されているように、両方のautorun
関数は初期化時に一度実行されます。これは、for
ループがない場合に見られる全てです。
reduceEnergy
アクションを使用してenergyLevel
を変更するfor
ループを実行すると、autorun
関数がオブザーバブル状態の変化を検出するたびに、新しいログエントリが表示されます。
「Energy level」関数の場合、これは
energyLevel
オブザーバブルが変更されるたびに、合計10回です。「Now I'm hungry」関数の場合、これは
isHungry
コンピュートが変更されるたびに、1回のみです。
Reaction
使用方法
reaction(() => value, (value, previousValue, reaction) => { sideEffect }, options?)
.
reaction
はautorun
に似ていますが、どのオブザーバブルが追跡されるかについて、より詳細な制御を提供します。2つの関数を受け取ります。最初のデータ関数は追跡され、2番目の効果関数の入力として使用されるデータが返されます。副作用は、データ関数でアクセスされたデータのみに反応することに注意することが重要です。これは、効果関数で実際に使用されるデータよりも少ない可能性があります。
一般的なパターンは、副作用に必要なものをデータ関数で生成し、その方法で効果がトリガーされるタイミングをより正確に制御することです。デフォルトでは、効果関数をトリガーするために、データ関数の結果を変更する必要があります。autorun
とは異なり、副作用は初期化時に一度実行されず、データ式が初めて新しい値を返す後でのみ実行されます。
例:データ関数と効果関数
以下の例では、リアクションはisHungry
が変更されたときにのみ一度トリガーされます。効果関数で使用されるgiraffe.energyLevel
の変更は、効果関数が実行される原因になりません。これにもreaction
で反応させたい場合は、データ関数でもアクセスして返す必要があります。
import { makeAutoObservable, reaction } from "mobx"
class Animal {
name
energyLevel
constructor(name) {
this.name = name
this.energyLevel = 100
makeAutoObservable(this)
}
reduceEnergy() {
this.energyLevel -= 10
}
get isHungry() {
return this.energyLevel < 50
}
}
const giraffe = new Animal("Gary")
reaction(
() => giraffe.isHungry,
isHungry => {
if (isHungry) {
console.log("Now I'm hungry!")
} else {
console.log("I'm not hungry!")
}
console.log("Energy level:", giraffe.energyLevel)
}
)
console.log("Now let's change state!")
for (let i = 0; i < 10; i++) {
giraffe.reduceEnergy()
}
出力
Now let's change state!
Now I'm hungry!
Energy level: 40
When
使用方法
when(predicate: () => boolean, effect?: () => void, options?)
when(predicate: () => boolean, options?): Promise
when
は、指定された述語関数がtrue
を返すまで、それを監視して実行します。それが発生すると、指定された効果関数が実行され、オートランナーは破棄されます。
when
関数はディスポーザを返し、2番目のeffect
関数を渡さない限り、手動でキャンセルできます。その場合はPromise
を返します。
例:リアクティブな方法で処理を破棄する
when
は、リアクティブな方法で処理を破棄またはキャンセルするのに非常に役立ちます。例えば
import { when, makeAutoObservable } from "mobx"
class MyResource {
constructor() {
makeAutoObservable(this, { dispose: false })
when(
// Once...
() => !this.isVisible,
// ... then.
() => this.dispose()
)
}
get isVisible() {
// Indicate whether this item is visible.
}
dispose() {
// Clean up some resources.
}
}
isVisible
がfalse
になるとすぐに、dispose
メソッドが呼び出され、MyResource
のクリーンアップが行われます。
await when(...)
effect
関数が提供されない場合、when
はPromise
を返します。これはasync / await
とよく組み合わさり、オブザーバブル状態の変更を待つことができます。
async function() {
await when(() => that.isVisible)
// etc...
}
when
を早期にキャンセルするには、それ自体によって返されるpromiseに対して.cancel()
を呼び出すことができます。
ルール
リアクティブコンテキストに適用されるいくつかのルールがあります。
- オブザーバブルが変更された場合、影響を受けるリアクションはデフォルトで直ちに(同期的に)実行されます。ただし、現在の最上位(トランス)アクションの終了前には実行されません。
- Autorunは、提供された関数の同期実行中に読み取られたオブザーバブルのみを追跡しますが、非同期に発生するものは何も追跡しません。
- Autorunは、アクションは常に追跡されないため、autorunによって呼び出されたアクションによって読み取られたオブザーバブルは追跡しません。
MobXが正確に何に反応し、何に反応しないかについての詳細な例については、リアクティビティの理解セクションを参照してください。トラッキングの仕組みに関するより詳細な技術的な説明については、ブログ記事Becoming fully reactive: an in-depth explanation of MobXをお読みください。
反応は必ず破棄する
autorun
、reaction
、when
に渡される関数は、それらが監視するすべてのオブジェクトが自身ガベージコレクションされる場合にのみ、ガベージコレクションされます。原則として、それらは使用しているオブザーバブルにおける新たな変更が永遠に起こるのを待ち続けます。永遠に経過するまで待つことを止めるには、それらはすべて、それらを停止し、使用したオブザーバブルの購読を解除するために使用できる破棄関数(ディスポーザ関数)を返します。
const counter = observable({ count: 0 })
// Sets up the autorun and prints 0.
const disposer = autorun(() => {
console.log(counter.count)
})
// Prints: 1
counter.count++
// Stops the autorun.
disposer()
// Will not print.
counter.count++
これらのメソッドから返される破棄関数を、その副作用が不要になった時点で常に使用することを強くお勧めします。そうしないと、メモリリークが発生する可能性があります。
reaction
およびautorun
のエフェクト関数に第2引数として渡されるreaction
引数は、reaction.dispose()
を呼び出すことによっても、反応を早期にクリーンアップするために使用できます。
例:メモリリーク
class Vat {
value = 1.2
constructor() {
makeAutoObservable(this)
}
}
const vat = new Vat()
class OrderLine {
price = 10
amount = 1
constructor() {
makeAutoObservable(this)
// This autorun will be GC-ed together with the current orderline
// instance as it only uses observables from `this`. It's not strictly
// necessary to dispose of it once an OrderLine instance is deleted.
this.disposer1 = autorun(() => {
doSomethingWith(this.price * this.amount)
})
// This autorun won't be GC-ed together with the current orderline
// instance, since vat keeps a reference to notify this autorun, which
// in turn keeps 'this' in scope.
this.disposer2 = autorun(() => {
doSomethingWith(this.price * this.amount * vat.value)
})
}
dispose() {
// So, to avoid subtle memory issues, always call the
// disposers when the reactions are no longer needed.
this.disposer1()
this.disposer2()
}
}
反応は控えめに使用する!
既に述べたように、反応を頻繁に作成することはありません。アプリケーションがこれらのAPIを直接使用せず、反応が構築される唯一の方法は、例えばmobx-reactバインディングのobserver
を介して間接的に行われる可能性が非常に高いです。
反応を設定する前に、それが次の原則に準拠しているかどうかを最初に確認することをお勧めします。
- 原因と結果の直接的な関係がない場合にのみ反応を使用する:非常に限られた一連のイベント/アクションに応じて副作用が発生する必要がある場合、それらの特定のアクションから直接効果をトリガーする方が明確になることがよくあります。たとえば、フォームの送信ボタンを押すと、投稿するネットワークリクエストが発生する必要がある場合、反応を介して間接的に行うのではなく、
onClick
イベントに応答して直接この効果をトリガーする方が明確です。対照的に、フォームの状態に加えた変更はすべて自動的にローカルストレージに保存される必要がある場合、反応は非常に役立ちます。これにより、個々のonChange
イベントごとにこの効果をトリガーする必要がなくなります。 - 反応は他のオブザーバブルを更新すべきではない:反応が他のオブザーバブルを変更しますか?答えがイエスの場合、通常、更新するオブザーバブルは、
computed
値として代わりに注釈付けする必要があります。たとえば、todoのコレクションが変更された場合、remainingTodos
の量を計算するために反応を使用するのではなく、remainingTodos
を計算値として注釈付けします。これにより、はるかに明確でデバッグしやすいコードになります。反応は新しいデータを計算するのではなく、効果を引き起こすだけです。 - 反応は独立している必要がある:コードが他の反応が先に実行されることに依存していますか?その場合、おそらく最初のルールに違反しているか、作成しようとしている新しい反応は、それが依存している反応にマージする必要があります。MobXは、反応が実行される順序を保証しません。
上記の原則に当てはまらない現実世界のシナリオがあります。そのため、それらは原則であり、法則ではありません。しかし、例外はまれなので、最後の手段としてのみ違反してください。
オプション {🚀}
autorun
、reaction
、when
の動作は、上記のようにoptions
引数を渡すことでさらに微調整できます。
name
この文字列は、SpyイベントリスナーおよびMobX開発者ツールでのこの反応のデバッグ名として使用されます。
fireImmediately
(reaction)
effect関数を最初のdata関数の最初の実行後にすぐにトリガーする必要があるかどうかを示すブール値。デフォルトはfalse
です。
delay
(autorun, reaction)
エフェクト関数を調整するために使用できるミリ秒数。0(デフォルト)の場合、調整は行われません。
timeout
(when)
when
が待つ時間制限を設定します。期限が過ぎると、when
は拒否/スローします。
signal
AbortSignalオブジェクトのインスタンス。破棄の代替方法として使用できます。when
のプロミス版で使用する場合、プロミスは「WHEN_ABORTED」エラーで拒否されます。
onError
デフォルトでは、反応内でスローされた例外はログに記録されますが、それ以上はスローされません。これは、1つの反応の例外が、他の可能性のある無関係な反応のスケジュールされた実行を妨げないようにするためです。これにより、反応は例外から回復することもできます。例外をスローしても、MobXによって行われる追跡は中断されないため、例外の原因が削除されれば、反応のその後の実行は正常に完了する可能性があります。このオプションでは、その動作を上書きできます。グローバルエラーハンドラーを設定するか、configureを使用してエラーのキャッチを完全に無効にすることができます。
scheduler
(autorun, reaction)
autorun関数の再実行のスケジュール方法を決定するカスタムスケジューラを設定します。たとえば、{ scheduler: run => { setTimeout(run, 1000) }}
のように、将来のある時点で呼び出す必要がある関数を取得します。
equals
: (reaction)
デフォルトでcomparer.default
に設定されます。指定されている場合、この比較関数は、data関数によって生成された以前の値と次の値を比較するために使用されます。この関数がfalseを返す場合にのみ、effect関数が呼び出されます。
組み込みの比較関数セクションを確認してください。