JavaScript
React
mobx

mobx-state-tree マニュアル

More than 1 year has passed since last update.

1.0.1に対応しました。

コンセプト

mobx-state-treeはデータをどのように構成し、どのように更新するかを問題としていて、以下のことが主な制約になる。

  • 型定義したモデルに当てはまらないデータは生成出来ない
  • 定義したアクション以外では状態は更新は出来ない
  • データの参照は観測され保護される
  • データの更新は全て追跡可能

基礎

ストアは、型定義がされたモデルとそれを更新する関数アクションなどを持ちます。
このストアからインスタンスを生成し、そこからスナップショットから生成されます。

import { onSnapshot, types } from 'mobx-state-tree'

const model = {title: types.string}

const actions = self => {
  return {
    setTitle (title) { self.title = title }
  }
}

const Store = types.model('store', model).actions(actions)

const snapshot = {title: 'title'}

const store = Store.create(snapshot)

onSnapshot(store, snapshot => {
  console.dir(snapshot) // { title: 'new title' }
})

store.setTitle('new title')

モデル

types.model を用いて、モデルを定義する。

const model = {title: types.string}

const Store = types.model('store', model)

// const Model = types.model('model', {title: types.string})

この'store'はデバッグに使用されるストアの名前であって、省略することもできます。

const Model = types.model({title: types.string})

アクション

actions を用いて、アクションを定義する。

const model = {title: types.string}

const actions = self => {
  return {
    setTitle (title) { self.title = title }
  }
}

const Store = types.model('store', model).actions(actions)

// const Store = types.model('model', {title: types.string}).actions(self => ({setTitle (title) { self.title = title }}))

ストアの状態はアクションで更新できる。だから、以下はエラーになる。

const model = {title: types.string}

const Store = types.model('store', model)

const snapshot = {title: 'title'}

const store = Store.create(snapshot)

store.title = 'next' // error!

インスタンス

create関数を用いてインスタンスを生成する。

const model = {title: types.string}

const Store = types.model('store', model)

const snapshot = {title: 'title'}

const store = Store.create(snapshot)

ここでインスタンスとはストアから生成されるObservable Objectのことで、スナップショットとはその時での状態のこと。

モデルに存在しないデータはスナップショットに含まれない。

import { types, getSnapshot } from 'mobx-state-tree'

const model = {title: types.string}

const actions = self => {
  return {
    setTitle (title) { self.title = title }
  }
}

const Store = types.model('store', model).actions(actions)

const snapshot = {title: 'title', ignore: 'title'}

const store = Store.create(snapshot)

const newSnapshot = getSnapshot(store)

console.log(newSnapshot) // {title: 'title'}

デバッグ

const json = store.toJSON() 

インスタンスはいくつかの関数を持っていて、toJSON関数を用いるとJSONデータを生成してくれる。※これはデバッグのみで使用する。

スナップショット

onSnapshot関数はインスタンスが更新され新しいスナップショットが生成されると実行される。

import { types, onSnapshot } from 'mobx-state-tree'

const model = {title: types.string}

const actions = self => {
  return {
    setTitle (title) { self.title = title }
  }
}

const Store = types.model('store', model).actions(actions)

const snapshot = {title: 'title'}

const store = Store.create(snapshot)

onSnapshot(store, snapshot => {
  console.dir(snapshot) // { title: 'new title' }
})

store.setTitle('new title') // run onSnapshot

スナップショットからインスタンスを生成することも、その逆も可能。

getSnapshot関数を用いると、ストアのインスタンスからスナップショットを取得できる。

applySnapshot関数を用いると、インスタンスにスナップショットを適用して、その時点での状態に巻き戻すことができる。

import { types, applySnapshot } from 'mobx-state-tree'

const model = {title: types.string}

const actions = self => {
  return {
    setTitle (title) { self.title = title }
  }
}

const Store = types.model('store', model).actions(actions)

const snapshot = {title: 'title'}

const store = Store.create(snapshot)

store.setTitle('new title')

console.log(store.title) // 'new title'

applySnapshot(store, snapshot)

console.log(store.title) // 'title'

モデルは型定義をする必要がある。

const Store = types.model('store', {
  title: types.string
})

複合型

types.model(object)
オブジェクトに作用するプロパティとアクションを持つ型

types.array(type)
指定された型の配列

types.map(type)
指定された型のマップ(Map)

types.objectは存在せず、Observable Objectでないオブジェクトは定義できない。
スナップショットには含まれないが、アクションを用いて状態を持たせることはできる。

import { types } from 'mobx-state-tree'

const model = {title: types.string}

const actions = self => {
  self.data = 'data'
  return {
    setTitle (title) { self.title = title }
  }
}

const Store = types.model('store', model).actions(actions)

プリミティブ型

プリミティブ型は以下の4つのみ。

types.string
types.number
types.boolean
types.Date

ユーティリティタイプ

例えば、types.optionalを用いることで、スナップショットに定義されているデータが存在しない場合、デフォルト値を設定できる。

const Store = types.model({
  title: types.optional(types.string, 'default title')
}

また、types.maybeを用いると、null値を設定してくれる。

const Store = types.model({
  title: types.maybe(types.string)
}

他は省略。

デフォルト

デフォルト値が存在し型を定義しなくない場合は値を設定する。

const Store = types.model({
  title: 'default title'
}

この時、'default title'はデフォルト値になる。

ただし、型はチェックされるしプロパティの更新された場合はスナップショットが生成される。

const Store = types.model({
  title: 'default title'
}

cont store = Store.create({title: 20}) // error

ツリー構造

複数のモデルからツリー構造のモデルを構成する。このとき、ツリー構造内部のモデルをノードと定義する。

const StoreA = types.model('storeA', {
  a: types.string
})

const StoreB = types.model('storeB', {
  b: types.string
})

const Tree = types.model('tree', {
  storeA: StoreA,
  storeB: StoreB
})

単純な話だとストアどは再利用できる。

Observable Arrayを実現するにはこのようにする。

const Message = types.model('message', {
  title: types.string
})

const User = types.model('user', {
  messages: types.array(Message)
})

const user = User.create({
  messages: [
    {title: 'title-a'},
    {title: 'title-b'}
  ]
})

これらはひとつのツリーに定義することも可能。

const User = types.model('user', {
  messages: types.array(
    types.model('message', {
      title: types.string
    })
  )
})

アクション

アクションには以下の制約がある。

  • アクションを使用せずにノードを変更しようとすると、エラーとなる。
  • アクションは他のツリーに属するモデルを変更できない
  • アクションで非同期的に状態を更新することはできない

これはエラーになるので注意する。

const {types} = require('mobx-state-tree')

const model = {title: types.string}

const actions = self => {
  return {
    setTitleAsync (title) {
      setTimeout(() => { self.title = title }, 10)
    }
  }
}

const Store = types.model('store', model).actions(actions)

const snapshot = {title: 'title'}

const store = Store.create(snapshot)

store.setTitleAsync('new title') // error!

非同期のアクションを定義するときは、そのアクションから同期的なアクションを呼び出すようにします。

const {types} = require('mobx-state-tree')

const model = {title: types.string}

const actions = self => {
  return {
    setTitleAsync (title) {
      setTimeout(() => { self.setTitle(title) }, 10) // call setTitle
    },
    setTitle (title) { self.title = title }
  }
}

const Store = types.model('store', model).actions(actions)

const snapshot = {title: 'title'}

const store = Store.create(snapshot)

store.setTitleAsync('new title')

ライフサイクル

インスタンスの生成やプロパティの削除にはライフサイクルが存在する。

import { types } from 'mobx-state-tree'

const model = {title: types.string}

const actions = () => {
  return {
    afterCreate () { console.log('afterCreate') },
    postProcessSnapshot (snapshot) { return snapshot }
  }
}

const preProcessSnapshot = snapshot => snapshot

const Store = types.model(model).actions(actions).preProcessSnapshot(preProcessSnapshot)

const snapshot = {title: 'title'}

const store = Store.create(snapshot)

ライフサイクルのうち preProcessSnapshot だけは .preProcessSnapshot となっている。

API

getSnapshot関数やonSnapshot関数など以外にも他にも沢山ある。

Observable Objectの読み書きの制約を解除するunprotect関数や、ReduxStoreを生成するasReduxStoreなどもある。

redux-devtools-extensionを用いることもできる。

const store = Store.create({})

const {connectReduxDevtools} = require('mobx-state-tree')
const remotedev = require('remotedev')
connectReduxDevtools(remotedev, model)

最後に

MSTの型チェックは処理を停止します。本番環境ではこれらを無効にする為に、環境変数NODE_ENVproductionに設定してください。

https://webpack.js.org/plugins/environment-plugin/#usage

JSONからMSTのモデルを生成できるサービスがあります。
https://github.com/mobxjs/mobx-state-tree