LoginSignup
52
38

More than 5 years have passed since last update.

次のReact状態管理 MobXのStore考察

Last updated at Posted at 2017-02-28

先日の投稿に続き第2弾です。MobXではReduxと違い、複数のStoreが存在し、複数のProviderを保持出来ます。ReduxにもProviderがありますが、それはコンポーネントルートに一つあるのみで、初期設計が終われば普段意識することはありません。

MobXでは、Storeをどこで生成し、どうProviderに与えるのが良い設計なのか。この観点に踏み込んだ記事を発見できていないため、独自方針ですが綴ることにしました。(優しいマサカリをお待ちしてます)

Redux踏襲パターン

Redux実装経験者も、そうでない方も、これが一番わかり易いかと思います。「Providerを一つしかもたない」です。こうしてしまえば、全てのコンポーネントが全ての状態にアクセス出来る状態になりますし、Reduxの1Storeと並列で考えることが出来ます。シンプルさを求めるなら、これで十分でしょう。

routes.js
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用コンポーネントを定義しています。

routes.js
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>
  )
}

providerA.js
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 を発行すれば良さそうです。

providerA.js
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>
    )
  }
}
52
38
3

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
52
38