LoginSignup
36
27

More than 5 years have passed since last update.

MobXの observable と action について

Last updated at Posted at 2017-03-02

先日の投稿に続き第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++

image

36
27
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
27