リアクティビティの理解
MobXは通常、期待どおりに反応します。つまり、使用例の90%ではMobXは「ただ動く」はずです。ただし、期待どおりに動作しないケースに遭遇することがあります。その時点で、MobXが何に反応するかをどのように判断するかを理解することは非常に重要になります。
MobXは、追跡された関数の実行中に読み取られる、既存のobservableなプロパティに反応します。
- 「読み取り」とは、オブジェクトのプロパティを間接参照することです。これは、「ドットで接続する」(例:
user.name
)またはブラケット表記(例:user['name']
、todos[3]
)または分割代入(例:const {name} = user
)を使用して行うことができます。 - 「追跡された関数」とは、
computed
の式、observer
のReact関数コンポーネントのレンダリング、observer
ベースのReactクラスコンポーネントのrender()
メソッド、およびautorun
、reaction
、およびwhen
の最初のパラメーターとして渡される関数のことです。 - 「実行中」とは、関数が実行されている間に読み取られるobservableのみが追跡されることを意味します。これらの値が追跡された関数によって直接的または間接的に使用されるかどうかは関係ありません。ただし、関数から「生成」されたものは追跡されません(例:
setTimeout
、promise.then
、await
など)。
言い換えれば、MobXは以下には反応しません
- observableから取得されたが、追跡された関数の外にある値
- 非同期的に呼び出されたコードブロックで読み取られるobservable
MobXは値ではなくプロパティアクセスを追跡します
上記のルールを例で詳しく説明すると、次のobservableなインスタンスがあるとします。
class Message {
title
author
likes
constructor(title, author, likes) {
makeAutoObservable(this)
this.title = title
this.author = author
this.likes = likes
}
updateTitle(title) {
this.title = title
}
}
let message = new Message("Foo", { name: "Michel" }, ["Joe", "Sara"])
メモリ内では、次のようになります。緑色のボックスはobservableなプロパティを示します。値自体はobservableではないことに注意してください!
MobXが基本的に行っていることは、関数で使用する矢印を記録することです。その後、これらの矢印の1つが変更されたとき、つまり他のものを参照し始めたときに、再実行されます。
例
たくさんの例(上記のmessage
変数に基づいています)でそれを示しましょう。
正しい:追跡された関数内での間接参照
autorun(() => {
console.log(message.title)
})
message.updateTitle("Bar")
これは期待どおりに反応します。.title
プロパティはautorunによって間接参照され、その後変更されたため、この変更が検出されます。
追跡された関数内でtrace()
を呼び出すことで、MobXが何を追跡するかを確認できます。上記の関数の場合、次の出力になります。
import { trace } from "mobx"
const disposer = autorun(() => {
console.log(message.title)
trace()
})
// Outputs:
// [mobx.trace] 'Autorun@2' tracing enabled
message.updateTitle("Hello")
// Outputs:
// [mobx.trace] 'Autorun@2' is invalidated due to a change in: 'Message@1.title'
Hello
また、getDependencyTree
を使用して、内部依存関係(またはオブザーバー)ツリーを取得することもできます。
import { getDependencyTree } from "mobx"
// Prints the dependency tree of the reaction coupled to the disposer.
console.log(getDependencyTree(disposer))
// Outputs:
// { name: 'Autorun@2', dependencies: [ { name: 'Message@1.title' } ] }
不正解:observableでない参照の変更
autorun(() => {
console.log(message.title)
})
message = new Message("Bar", { name: "Martijn" }, ["Felicia", "Marcus"])
これは反応しません。message
は変更されましたが、message
はobservableではなく、observableを参照する単なる変数ですが、変数(参照)自体はobservableではありません。
不正解:追跡された関数の外での間接参照
let title = message.title
autorun(() => {
console.log(title)
})
message.updateMessage("Bar")
これは反応しません。message.title
はautorun
の外で間接参照され、間接参照の瞬間のmessage.title
の値(文字列"Foo"
)のみが含まれています。title
はobservableではないため、autorun
は反応しません。
正しい:追跡された関数内での間接参照
autorun(() => {
console.log(message.author.name)
})
runInAction(() => {
message.author.name = "Sara"
})
runInAction(() => {
message.author = { name: "Joe" }
})
これは両方の変更に反応します。author
とauthor.name
の両方がドットで結ばれているため、MobXはこれらの参照を追跡できます。
action
の外で変更を行うには、ここでrunInAction
を使用する必要があったことに注意してください。
不正解:observableなオブジェクトへのローカル参照を追跡せずに保存
const author = message.author
autorun(() => {
console.log(author.name)
})
runInAction(() => {
message.author.name = "Sara"
})
runInAction(() => {
message.author = { name: "Joe" }
})
最初の変更は取得されます。message.author
とauthor
は同じオブジェクトであり、.name
プロパティはautorunで間接参照されます。ただし、2番目の変更は取得されません。これは、message.author
の関係がautorun
によって追跡されていないためです。Autorunは依然として「古い」author
を使用しています。
よくある落とし穴:console.log
autorun(() => {
console.log(message)
})
// Won't trigger a re-run.
message.updateTitle("Hello world")
上記の例では、更新されたメッセージタイトルは、autorun内で使用されていないため、印刷されません。autorunは、observableではなく変数であるmessage
にのみ依存しています。言い換えれば、MobXに関する限り、title
はautorun
で使用されていません。
Webブラウザのデバッグツールでこれを使用すると、結局のところ、更新されたtitle
の値を見つけることができるかもしれませんが、これは誤解を招きます。結局のところ、autorunは最初に呼び出されたときに1回実行されました。これは、console.log
が非同期関数であり、オブジェクトが後でフォーマットされる場合に発生します。これは、デバッグツールバーでタイトルを追跡すると、更新された値を見つけることができることを意味します。ただし、autorun
は更新を追跡しません。
これを機能させるには、常にイミュータブルなデータまたは防御的コピーをconsole.log
に渡すようにする必要があります。したがって、以下の解決策はすべてmessage.title
の変更に反応します。
autorun(() => {
console.log(message.title) // Clearly, the `.title` observable is used.
})
autorun(() => {
console.log(mobx.toJS(message)) // toJS creates a deep clone, and thus will read the message.
})
autorun(() => {
console.log({ ...message }) // Creates a shallow clone, also using `.title` in the process.
})
autorun(() => {
console.log(JSON.stringify(message)) // Also reads the entire structure.
})
正:追跡関数での配列プロパティへのアクセス
autorun(() => {
console.log(message.likes.length)
})
message.likes.push("Jennifer")
これは期待どおりに反応します。.length
はプロパティとしてカウントされます。これは配列のあらゆる変更に反応することに注意してください。配列は、(observableオブジェクトやマップのように)インデックス/プロパティごとに追跡されるのではなく、全体として追跡されます。
不正:追跡関数での範囲外のインデックスへのアクセス
autorun(() => {
console.log(message.likes[0])
})
message.likes.push("Jennifer")
配列インデックスはプロパティアクセスとしてカウントされるため、これは上記のサンプルデータで反応します。ただし、提供されたindex < length
の場合のみです。MobXは、まだ存在しない配列インデックスを追跡しません。したがって、配列インデックスへのアクセスは常に.length
チェックで保護してください。
正:追跡関数での配列関数へのアクセス
autorun(() => {
console.log(message.likes.join(", "))
})
message.likes.push("Jennifer")
これは期待どおりに反応します。配列をミューテートしないすべての配列関数は、自動的に追跡されます。
autorun(() => {
console.log(message.likes.join(", "))
})
message.likes[2] = "Jennifer"
これは期待どおりに反応します。すべての配列インデックスの代入が検出されますが、index <= length
の場合のみです。
不正:observableを使用するが、そのプロパティにはアクセスしない
autorun(() => {
message.likes
})
message.likes.push("Jennifer")
これは反応しません。単に、likes
配列自体がautorun
で使用されておらず、配列への参照のみが使用されているためです。対照的に、message.likes = ["Jennifer"]
は取得されます。そのステートメントは配列を変更するのではなく、likes
プロパティ自体を変更します。
正:まだ存在しないマップエントリの使用
const twitterUrls = observable.map({
Joe: "twitter.com/joey"
})
autorun(() => {
console.log(twitterUrls.get("Sara"))
})
runInAction(() => {
twitterUrls.set("Sara", "twitter.com/horsejs")
})
これは反応します。Observableマップは、存在しない可能性のあるエントリの監視をサポートします。これは最初はundefined
を出力することに注意してください。最初にtwitterUrls.has("Sara")
を使用することで、エントリの存在を確認できます。したがって、動的にキー設定されたコレクションのProxyサポートがない環境では、常にobservableマップを使用してください。Proxyサポートがある場合は、observableマップを使用することもできますが、プレーンオブジェクトを使用することもできます。
MobXは非同期的にアクセスされたデータを追跡しません
function upperCaseAuthorName(author) {
const baseName = author.name
return baseName.toUpperCase()
}
autorun(() => {
console.log(upperCaseAuthorName(message.author))
})
runInAction(() => {
message.author.name = "Chesterton"
})
これは反応します。author.name
がautorun
に渡された関数自体によって逆参照されない場合でも、MobXはupperCaseAuthorName
で発生する逆参照を追跡します。なぜなら、autorunの実行中に発生するためです。
autorun(() => {
setTimeout(() => console.log(message.likes.join(", ")), 10)
})
runInAction(() => {
message.likes.push("Jennifer")
})
autorun
の実行中にobservableがアクセスされなかったため、これは反応しません。非同期関数であるsetTimeout
中にのみアクセスされました。
「非同期アクション」セクションも確認してください。
observableではないオブジェクトプロパティの使用
autorun(() => {
console.log(message.author.age)
})
runInAction(() => {
message.author.age = 10
})
これは、Proxyをサポートする環境でReactを実行している場合は反応します。これは、observable
またはobservable.object
で作成されたオブジェクトに対してのみ行われることに注意してください。クラスインスタンスの新しいプロパティは、自動的にobservableにはなりません。
Proxyサポートのない環境
これは反応しません。MobXはobservableプロパティのみを追跡でき、上記の「age」はobservableプロパティとして定義されていません。
ただし、MobXによって公開されているget
メソッドとset
メソッドを使用して、これを回避することが可能です。
import { get, set } from "mobx"
autorun(() => {
console.log(get(message.author, "age"))
})
set(message.author, "age", 10)
[Proxyサポートなし]不正:まだ存在しないobservableオブジェクトプロパティの使用
autorun(() => {
console.log(message.author.age)
})
extendObservable(message.author, {
age: 10
})
これは反応しません。MobXは、追跡が開始されたときに存在しなかったobservableプロパティには反応しません。2つのステートメントが入れ替えられた場合、または他のobservableがautorun
を再実行させた場合、autorun
はage
の追跡も開始します。
[Proxyサポートなし]正:オブジェクトの読み取り/書き込みにMobXユーティリティを使用する
Proxyサポートがない環境にいて、observableオブジェクトを動的コレクションとして使用したい場合は、MobXのget
およびset
APIを使用して処理できます。
以下も反応します
import { get, set, observable } from "mobx"
const twitterUrls = observable.object({
Joe: "twitter.com/joey"
})
autorun(() => {
console.log(get(twitterUrls, "Sara")) // `get` can track not yet existing properties.
})
runInAction(() => {
set(twitterUrls, { Sara: "twitter.com/horsejs" })
})
詳細については、コレクションユーティリティAPIを確認してください。
要するに
MobXは、追跡された関数の実行中に読み取られる、既存のobservableなプロパティに反応します。