Edited at

MobXの observable と action について

More than 1 year has passed since last update.

先日の投稿に続き第3弾です。前回はMobXAPIの話を差し置いて、いきなりReactMobXの設計論になってしまいました。本稿では個人的にMobXのちょっと分かり辛い・気をつけないといけないなと感じた、基本の部分を綴っています。ゆるい感じでピックアップしながら、気ままに記事を書いていこうと思います。


【DevTools】

もうMobXのコードを書いてみた方で、DevToolsを導入していない方は是非いれてみてください。こちらのページで<DevTools / >をマウントして導入する方法が紹介されていますが、ChromeExtensionをいれておけば、マウント無しで同じ機能が利用出来る様になります。本稿の記事を読み解くにあたり、役にたつはずです。


observable型について

observable値には、JSプリミティブ、参照、プレーンオブジェクト、クラスインスタンス、配列、マップなどがあります。@observableは、常に最適な observable型を作成しようとします。なので、何となしで @observableな値を作ってしまうと危険です。

observable型は、階層をどこまで辿って追跡可能な値とするのか、modifiers で指定することが出来ます。modifiersには「ref」「shallow」「deep」があり、何も指定しなければ、常に.deepとなります。


store.js

class Store {

@observable.ref collectionRef = []
@observable.shallow collectionShallow = []
@observable/*.deep*/ collectionDeep = []
}

.refは最も浅い追跡をします。下記の様な挙動です。Objectの参照が変わることをフックに、observerに伝達させます。


store.js

class Store {

@observable.ref collection = []
@action editCollection = () => {
// 参照が変わるので、伝達する(新しい配列を参照)
this.collection = [{ name: 'myName' }]
// 参照はそのままなので、伝達しない
this.collection[0] = { name: 'myName' }
}
}

.shallowは一階層目まで追跡、変更が伝達されます。参照の変更においても伝達されます。


store.js

class Store {

@observable.shallow user = {
name: 'myName',
friends: [{ name: 'firendName' }]
}
@action editUser = () => {
// 1階層目なので、伝達する
this.user.name = 'newName'
// 2階層目より深いので、伝達しない
this.user.friends[0].name = 'newFriendName'
// 参照が変わっているので、伝達する
this.user = { name: 'myName', friends: [{ name: 'newFriendName' }]}
}
}

.deepはどこまで深くなっても追跡、変更が伝達されます。デフォルトの挙動がこれです。気をつけなければいけないのが、サンプルコードの下部、新しいObjectを参照しているところです。


store.js

class Store {

@observable user = {
name: 'myName',
friends: [{ name: 'firendName' }]
}
@action editUser = () => {
// 伝達する(1度だけ)
this.user.friends[0].name = 'newFriendName'
// 伝達する(3度も)
this.user = { name: 'myName', friends: [{ name: 'newFriendName' }]}
}
}

deepで追跡しているため、以下の3点が observable値とみなされ、

observerへの伝達が3度も走ることになります。これはthis.userを参照している Reactコンポーネントで、3度リレンダリングされることを意味します。


  • this.user.name

  • this.user.friends

  • this.user.friends[0]

この点をきちんと把握しておかないと、知らず知らずのうちにパフォーマンスが劣化してしまいます。APIから巨大なjson配列を取得して @observable = [] などに代入すると、とんでもなく遅くなります。こういったものには @observable.ref = [] の様に、要件に応じた modifier を付与しましょう。

また上記は、伝達するかしないかの話であって、内容は変化しています。別コンポーネントの変化によって再レンダリングされた時に変化するので、その挙動に少し戸惑うかもしれません。


actionの存在意義

カッチリ書きたい人向け、と考えて良さそうです。

とりあえず useStrict(true) を宣言してみましょう。


main.js

import { useStrict } from 'mobx'

useStrict(true)

@action でデコレートされていない関数以外から observable値を変更しようとすると、いろいろ怒られます。useStrict(true) 宣言も @action デコレータの使用も、optional とのことですが、堅牢に作りたい場合やログを追える様になるので、個人的にはactionも、strictモードも使う派です。

Error: [mobx] Invariant failed: Since strict-mode is enabled, changing observed observable values outside actions is not allowed. Please wrap the code in an `action` if this change is intended. 


  • action を付与することで、stateを変更する関数であることを明示的に宣言する。

  • observable値を変更したり副作用を伴う関数は action 使用が推奨される。

  • DevToolsでログ出力される様になる。

  • strictモードを有効にしている場合、action を経由しないobservable値の変更は怒られる。

  • コールバックなどの副作用から、observable値を変更しようとする場合、runInAction 関数でラップしなければ怒られる。


  • runInAction 関数は第一引数にDevToolsのログを設定できる。

ちなみに、@actionデコレータも下記の様にログ用の文字列を指定することが出来ます。日本語もござれ。


someStore.js

@action('値を1追加') increment = () => this.count++