先日の投稿に続き第3弾です。前回はMobXAPIの話を差し置いて、いきなりReactMobXの設計論になってしまいました。本稿では個人的にMobXのちょっと分かり辛い・気をつけないといけないなと感じた、基本の部分を綴っています。ゆるい感じでピックアップしながら、気ままに記事を書いていこうと思います。
【DevTools】
もうMobXのコードを書いてみた方で、DevToolsを導入していない方は是非いれてみてください。こちらのページで<DevTools / >
をマウントして導入する方法が紹介されていますが、ChromeExtensionをいれておけば、マウント無しで同じ機能が利用出来る様になります。本稿の記事を読み解くにあたり、役にたつはずです。
observable型について
observable値には、JSプリミティブ、参照、プレーンオブジェクト、クラスインスタンス、配列、マップなどがあります。@observable
は、常に最適な observable型を作成しようとします。なので、何となしで @observable
な値を作ってしまうと危険です。
observable型は、階層をどこまで辿って追跡可能な値とするのか、modifiers で指定することが出来ます。modifiersには「ref」「shallow」「deep」があり、何も指定しなければ、常に.deep
となります。
class Store {
@observable.ref collectionRef = []
@observable.shallow collectionShallow = []
@observable/*.deep*/ collectionDeep = []
}
.ref
は最も浅い追跡をします。下記の様な挙動です。Objectの参照が変わることをフックに、observerに伝達させます。
class Store {
@observable.ref collection = []
@action editCollection = () => {
// 参照が変わるので、伝達する(新しい配列を参照)
this.collection = [{ name: 'myName' }]
// 参照はそのままなので、伝達しない
this.collection[0] = { name: 'myName' }
}
}
.shallow
は一階層目まで追跡、変更が伝達されます。参照の変更においても伝達されます。
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を参照しているところです。
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)
を宣言してみましょう。
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
デコレータも下記の様にログ用の文字列を指定することが出来ます。日本語もござれ。
@action('値を1追加') increment = () => this.count++