先日の投稿に続き第2弾です。MobXではReduxと違い、複数のStoreが存在し、複数のProviderを保持出来ます。ReduxにもProviderがありますが、それはコンポーネントルートに一つあるのみで、初期設計が終われば普段意識することはありません。
MobXでは、Storeをどこで生成し、どうProviderに与えるのが良い設計なのか。この観点に踏み込んだ記事を発見できていないため、独自方針ですが綴ることにしました。(優しいマサカリをお待ちしてます)
Redux踏襲パターン
Redux実装経験者も、そうでない方も、これが一番わかり易いかと思います。「Providerを一つしかもたない」です。こうしてしまえば、全てのコンポーネントが全ての状態にアクセス出来る状態になりますし、Reduxの1Storeと並列で考えることが出来ます。シンプルさを求めるなら、これで十分でしょう。
import StoreA from 'stores/storeA'
import StoreB from 'stores/storeB'
import StoreC from 'stores/storeC'
const stores = {
storeA: new StoreA(),
storeB: new StoreB(),
storeC: new StoreC()
}
export const Routes = () => {
return (
<Provider { ...stores }>
<Router history={ history }>
<Route path="/A" component={ ComponentA } />
<Route path="/B" component={ ComponentB } />
<Route path="/C" component={ ComponentC } />
</Router>
</Provider>
)
}
でもちょっと待ってください。もう一歩踏み込んで、Storeが大量になっていく状態を想像してみてください。「何十Storeにもなった時、アプリケーション初期化時に new Store して、それで良いの? 要らないStoreを確保してない?」ということです。
あなたのSPA、メモリ圧迫してませんか?
SPAで何かしらの状態管理実装を経験された方は、ある時点で気づくはず。Storeの状態は放置しておくと、パンパンになるということに。。画面を表示するうえで不要になった状態を解放する処理を、どこかに実装しなければいけません。もしこの観点を意識されていなかったのなら、非SPAでは直面しなかった問題が現れるのも、時間の問題です。
Reduxの場合、containerがその解放役を担えそうです(自分の場合そこに押し込む)。表示において保持しておく必要のない情報は、アンマウント時に適宜解放、どの条件をもって解放するかは、設計次第。というところかと思います。
MobXにおいては、Containerが存在しないため、上記機能を代替する層が必要になります。そこで、'react-router/Route'
'mobx-react/Provider'
'Store'
を密結合させるパターンが以下のRoute密結合パターンです。
Route密結合パターン
MobXで実装していると、srcディレクトリにおいて、components、stores、といったファイル群が必ず出来るかと思います。筆者はそれに加えて「providers」ディレクトリを確保し、ProviderでラップされたRoute用コンポーネントを定義しています。
import rootStore from 'stores/root'
import ProviderA form 'providers/providerA'
import ProviderB form 'providers/providerB'
import ProviderC form 'providers/providerC'
export const Routes = () => {
return (
<Provider rootStore={ rootStore }>
<Router history={ history }>
<Route path="/A" component={ ProviderA } />
<Route path="/B" component={ ProviderB } />
<Route path="/C" component={ ProviderC } />
</Router>
</Provider>
)
}
import { Component } from 'react'
import { Provider } from 'mobx-react'
import StoreA from '~/stores/storeA'
import ComponentA from '~/components/componentA'
export default class ProviderA extends Component {
componentWillMount() {
this.store = new StoreA()
}
componentWillUnmount() {
this.store = void 0 // アンマウント時に削除
}
render() {
return (
<Provider storeA={ this.store }>
<ComponentA />
</Provider>
)
}
}
かなり雑な例ですが、routingに併せて必要なstore生成を必要な分だけ、生成することが出来ていますね。解放手段も色々検討できそうです。
- マウントする度にAPIを叩きたくない
- アンマウント時に一部の状態だけ解放したい
- 閲覧環境のパフォーマンスを検証したうえで解放したい
こういった要件にも応えることが出来る providers層をあらかじめ持っておくことで、後々のメモリ圧迫危機を回避できそうです。また、MVCフレームワークのコントローラの様な立ち位置にあることが分かるかと思います。
ただし、全てのコンポーネントが全ての Store にアクセス出来る、という構造ではなくなります。コンポーネントがあるStoreを参照するためには、それに紐づけられた Provider に内包されていることが条件です。「ああ…こっちのコンポーネントでも、あのStoreの状態参照したい…」となったら、上階層の ProviderStore に migrate しましょう。解放、保持などの依頼は、該当の Provider で参照したい Store を inject し、action を発行すれば良さそうです。
import { Component } from 'react'
import { Provider } from 'mobx-react'
import StoreA from '~/stores/storeA'
import ComponentA from '~/components/componentA'
@inject(s => ({
retainAction: s.upperStore.someRetainAction
disposeAction: s.upperStore.someDisposeAction
}))
export default class ArticlesProvider extends Component {
componentWillMount() {
this.props.retainAction()
}
componentWillUnmount() {
this.props.disposeAction()
}
render() {
return (
<Provider articleStore={ this.store }>
<ComponentA />
</Provider>
)
}
}