React MobX における SingletonStoresの考察

[先日の投稿](http://qiita.com/Takepepe/items/cf22f1a17df30a76f170#_reference-1b178a4262d690dbd8f8)に続き第4弾です。MobXは柔軟で分かり易いため、多くを語らずともだいたいのことは出来てしまいます。

MobXはObservableな値を導入するためのライブラリであり、フレームワークではありません。実装がシンプルになる一方、設計方針と規約を明確にしなければ、独自設計が入り乱れ、保守の難しいコードになってしまいます。既出の設計論をみつつ、考慮出来るポイントをいくつか紹介していきます。

# 既存の React MobX 設計論

React-MobXにおける[コンポーネント実装の正](https://mobx.js.org/best/react-performance.html)は明確で、迷うことはほぼありません。小さく組み立てていき、あとは stateless か stateful かを選択するぐらいでしょう。MobXにおいて設計のキモは Store に集約するものだとすぐに気付きます。mobx.js.org でも  Store設計がどうあるべきか、という記事が掲載されています。https://mobx.js.org/best/store.html

**ここに記されている Singletonパターンは冒頭で明記されている通り、単なる1つの方法であり、`MobX = Singleton` ではないことを誤解なきようお願いします。**

# Singletonパターン考察

class定義ファイル内でインスタンスを1つ生成して export、class定義は exportしない。こうすることで、アプリケーションにおいて唯一のインスタンスが約束されます。以下の様な実装です。

```stores/singletons/WindowStore.js
class WindowStore {
}
const singleton = new WindowStore()
export default singleton
```

ウィンドウリサイズで observable値を変更するものに書き換えてみましょう。幅と高さが observableになります。

```stores/singletons/WindowStore.js
class WindowStore {
  @observable width
  @observable height
  constructor() {
    window.addEventListener('resize', e => this.onResize(e))
  }
  @action onResize = () => {
    this.width  = window.innerWidth
    this.height = window.innerHeight
  }
}
const singleton = new WindowStore()
export default singleton
```

ここで定義されている observable値を参照している異なる Storeをみてみましょう。computedで反応することが出来ます。

```stores/singletons/GlobalUIStore.js
import WindowStore from '~/stores/singletons/WindowStore.js'

export default class GlobalUIStore {
  @computed get windowArea() {
    return `${WindowStore.width * WindowStore.height}px`
  }
}
const singleton = new GlobalUIStore()
export default singleton
```

ただし、別Storeに密になりすぎたり、いくつものStoreを跨ぐ参照は控えた方が良いので、それぞれの役割を明確にしましょう。また、このままではテストコードが必要な場合に困ります。

# 参照しあうStoreの設計

先ほどのサンプルコードでは、画面サイズのモックを作成することが不可能でした。この点、少し工夫をすることでクリアすることが出来ます。

```stores/singletons/WindowStore.js
export class WindowStore { // ※1 exportしてしまう
  @observable width
  @observable height
  constructor(props) {
    Object.assign(this, props) // ※2 instantiate時に引数をとる
    window.addEventListener('resize', e => this.onResize(e))
  }
  @action onResize = () => {
    this.width  = window.innerWidth
    this.height = window.innerHeight
  }
}
const singleton = new WindowStore()
export default singleton // ※3 通常 importされるのはこっち
```

```stores/singletons/GlobalUIStore.js
// ※4 シングルトンインスタンスを import
import WindowStore from '~/stores/singletons/WindowStore.js' 

export class GlobalUIStore {
  constructor(props) {
    Object.assign(this, props) // ※6 instantiate時に引数をとる
  }
  @computed get windowArea() {
    // ※7 この段階ではシングルトンインスタンスを参照
    return `${this.WindowStore.width * this.WindowStore.height}px`
  }
}
// ※5 依存Store（シングルトンインスタンス）への参照を保持させる。（ブラウザ実行時の挙動）
const singleton = new GlobalUIStore({ WindowStore })
export default singleton
```

```stores/singletons/GlobalUIStore.test.js
// ※8 それぞれシングルトンインスタンスではなく class定義を import
import { WindowStore }   from '~/stores/singletons/WindowStore.js' 
import { GlobalUIStore } from '~/stores/singletons/GlobalUIStore.js' 

const globalUIStore = new GlobalUIStore({
  // ※9 モックを挿入することが出来る様になった
  WindowStore: new WindowStore({ width: 640, height: 480 })
})
// ...中略
it('Find screen area', () => {
  expect(globalUIStore.windowArea).to.be.eql("307200px")
})

```
class定義をexportしてしまっている段階で、シングルトンパターンとしては破綻していますが、ここは規約に定めて矯正する他なさそうです。Store同士の依存関係をコンストラクタで渡すことで、テスト可能なStoreにすることが出来ました。

# SingletonStore の inject

mobx-react の API である inject は、紐づけられた Providerが保持している Storeに限った参照をできるものだと思っていましたが、今のところそうでは無い様子です。SingletonStoreインスタンス を component で直接 inject しても反応するので、設計次第では活用出来るかもしれません。

```someComponent.js
import { inject } from 'mobx-react'
import SomeStore  from '~/stores/singletons/someStore'

const SomeComponent = ({ valueA, valueB, flag }) => {
  return (
    <div className="some-component">
      { flag ? valueA : valueB }
    </div>
  )
}

export default inject(() => ({
  valueA: SomeStore.observableValueA,
  valueB: SomeStore.observableValueB,
  flag:   SomeStore.observableFlag
}))(observer(SomeComponent))
```

# SingletonStore の instantiate順
これまでのサンプルコードの場合、SingletonStore を importした時点で初めてインスタンスが生成されます。そのため、参照関係が前後してしまうと、インスタンス生成前に参照エラーが発生します。SingletonStoreは一元管理し、エントリーポイントに近い位置で import しておくと良いです。

これまでのサンプルコードを、最終的なものに書き換えると以下の様になります。

```entry_point.js
import '~/stores/singletons'
```

```stores/singletons.js
import _SettingsStore  from '~/stores/singletons/settingsStore'
import _WindowStore    from '~/stores/singletons/windowStore'
import _SomeStore      from '~/stores/singletons/someStore'

export const SettingsStore = new _SettingsStore()
export const WindowStore   = new _WindowStore({ SettingsStore })
export const SomeStore     = new _SomeStore({ SettingsStore, WindowStore })
```

```stores/singletons/windowStore.js
export default class WindowStore {
  @observable width
  @observable height
  constructor(props) {
    Object.assign(this, props)
    window.addEventListener('resize', e => this.onResize(e))
  }
  @action onResize = () => {
    this.width  = window.innerWidth
    this.height = window.innerHeight
  }
}
// const singleton = new WindowStore() // ※各ファイルでは、インスタンスを生成しない
// export default singleton

```