Correct way of Creating multiple stores with mobx and injecting it into to a Component - ReactJs

24,463

Solution 1

This answer may be opinionated but it may help the community indirectly.
After a lot of research, I saw below approaches used in practice by many. General methods Have a root store that can act as a communication channel between stores.

Question 1: How to organise stores and inject them into the component?

Approach 1:

App.js

// Root Store Declaration
class RootStore {
    constructor() {
      this.userStore = new UserStore(this);
      this.authStore = new AuthStore(this);
    }
}    
const rootStore = new RootStore()

// Provide the store to the children
<Provider 
    rootStore={rootStore}
    userStore={rootStore.userStore}
    authStore={rootStore.authStore}
>
  <App />
</Provider>

Component.js

// Injecting into the component and using it as shown below
@inject('authStore', 'userStore')
@observer
class User extends React.Component {
    // only this.props.userStore.userVariable
}

Approach 2:

App.js

class RootStore {
    constructor() {
      this.userStore = new UserStore(this);
      this.authStore = new AuthStore(this);
    }
} 
const rootStore = new RootStore()

<Provider rootStore={rootStore}>
  <App />
</Provider>

Component.js

// Injecting into the component and using it as shown below
@inject(stores => ({
    userStore: stores.userStore,
    authStore: stores.authStore,
    })
)
@observer
class User extends React.Component {
    // no this.props.rootStore.userStore,userVariable here, 
    // only this.props.userStore.userVariable
}

Approach 1 and Approach 2 doesn't make any difference other than syntax difference. Okay! that is the injection part!

Question 2: How to have an inter-store communication? (Try to avoid it)

Now I know a good design keeps stores independent and less coupled. But somehow consider a scenario where I want the variable in UserStore to change if a certain variable in AuthStore is changed. Use Computed. This approach is common for both the above approaches

AuthStore.js

export class AuthStore {    
    constructor(rootStore) {
        this.rootStore = rootStore
        @computed get dependentVariable() {
          return this.rootStore.userStore.changeableUserVariable;                                      
        }
    }
}

I hope this helps the community. For more detailed discussion you can refer to the issue raised by me on Github

Solution 2

I would recommend you to have multiple stores, to avoid chaining of stores. As we do in our application:

class RootStore {
    @observable somePropUsedInOtherStores = 'hello';
}

class AuthStore {
    @observeble user = 'Viktor' ; 

    constructor(rootStore) {
        this.rootStore = rootStore;
    }

    // this will reevaluate based on this.rootStore.somePropUsedInOtherStores cahnge
    @computed get greeting() {
        return `${this.rootStore.somePropUsedInOtherStores} ${this.user}`
    }
}

const rootStore = new RootStore();

const stores = {
    rootStore,
    bankAccountStore: new BankAccountStore(rootStore),
    authStore = new AuthStore(rootStore) 
}

<Provider {...stores}>
  <App />
</Provider>

In such a manner you can access exactly the store you need, as mostly one store covers one domain instance. Still, both sub-stores are able to communicate to rootStore. Set its properties or call methods on it.

If you do not need a cross store communication - you may not need a rootStore at all. Remove it and don't pass to other stores. Just keep 2 siblings stores

Answering your question on injecting not a whole store, you may benefit from mapperFunction (like mapStateToProps in redux) docs here

@inject(stores => ({
        someProp: stores.rootStore.someProp
    })
)
@observer
class User extends React.Component {
// no props.rootStore here, only props.someProp

}

Solution 3

Initializing your RootStore directly in api.js file before passing it to Provider is sometimes not what you want. This can make injecting the instance of main store class harder into other js files:

Example 1:

app.js - Creates new instance before passing it to Provider:

//Root Store Declaration
class RootStore {
    constructor() {
      ...
    }
}    

const rootStore = new RootStore()

// Provide the store to the children
<Provider rootStore={rootStore}>
  <App />
</Provider>

Example 2:

RootStore.js - creates new instance directly in RootStore class:

// Root Store Declaration
class RootStore {
    constructor() {
      ...
    }
}    

export default new RootStore();

Example 1 compared to Example 2, makes harder to access/inject the store in another part of the application, like in Api.js described below.

Api.js file represents axios wrapper (in my case it handles global loading indicator):

import rootStore from '../stores/RootStore'; //right RootStore is simply imported

const axios = require('axios');
const instance = axios.create({
 ...
});

// Loading indicator
instance.interceptors.request.use(
    (request) => {
        rootStore.loadingRequests++;
        return request;
    },
    (error) => {
        rootStore.loadingRequests--; 
        return Promise.reject(error);
    }
)

And using React Hooks, you can inject the store that way:

import { observer, inject } from "mobx-react";
const YourComponent = ({yourStore}) => {
      return (
          ...
      )
}

export default inject('yourStore')(observer(YourComponent));
Share:
24,463

Related videos on Youtube

uneet7
Author by

uneet7

Hi! I am a Computer Science Graduate of 2019 Batch from IIT Ropar. I have served as the technical founder of my own startup Voylr and has been actively invloved in delivering the products. Currently, I work at $6m seed funded startup Covered by Sage as a software developer, managing/mentoring a team of 4. I am also actively involved in building the dev team for SAGE. Reach out to me? [email protected]

Updated on June 19, 2020

Comments

  • uneet7
    uneet7 almost 4 years

    As suggested here in the Mobx documentation I have created multiple stores in the following manner:

    class bankAccountStore {
      constructor(rootStore){
        this.rootStore = rootStore;
      }
    ...
    
    class authStore {
      constructor(rootStore){
        this.rootStore = rootStore;
      }
    ...
    

    And finally creating a root store in the following manner. Also I prefer to construct children stores within master's store constructor. Moreover, I found that sometimes my child store has to observe some data from parent store, so I pass this into child constructors

    class RootStore {
      constructor() {
        this.bankAccountStore = new bankAccountStore(this);
        this.authStore = new authStore(this);
      }
    }
    

    Providing to the App in following manner:

    <Provider rootStore={new RootStore()}>
      <App />
    </Provider>
    

    And injecting to the component in like this:

    @inject('rootStore') 
    @observer
    class User extends React.Component{
      constructor(props) {
        super(props);
        //Accessing the individual store with the help of root store
        this.authStore = this.props.rootStore.authStore;
      }
    }
    

    Is it the correct and efficient way to inject the root store everytime to the component even if it needs a part of the root store? If not how to inject auth Store to the user component?

    EDIT: I have made an answer concluding the github discussion. Link of the discussion provided in the answer

  • uneet7
    uneet7 about 5 years
    I got everything except the one thing . What's the point of creating empty rootStore and then passing it's reference to the other stores?
  • Shevchenko Viktor
    Shevchenko Viktor about 5 years
    Obviously rootStore is not empty and contains some functionality. From the best practices of separation of concerns it is better to let bankAccountStore and authStore know about rootStore (that will allow them to communicate) instead of letting them know about one another. So rootStore may play a role of a bus to allow other stores to communicate.
  • uneet7
    uneet7 about 5 years
    So the new rootStore() is the instance of the same rootStore class that is declared in the question. Correct?
  • uneet7
    uneet7 about 5 years
    If not can you add the declaration of class rootStore in your answer?
  • Shevchenko Viktor
    Shevchenko Viktor about 5 years
    Updated my answer
  • uneet7
    uneet7 about 5 years
    But what if I want to access data into the one store from another store? Because it is not priorly known what data will be shared among stores and after realizing it moving it to the root store is an overhead I guess.
  • Shevchenko Viktor
    Shevchenko Viktor about 5 years
    The main goal I want to point - prefer cross store communication over 3rd store, not direct, as this will allow you to better isolate stores from one another. I don't know the specifics of your app, but this is a development process, you see that something should work in another manner - your refactor. Yes this is overhead - but if your sores are dependent on one another - expect big troubles when you will have to refactor one or remove. When this may not be valid for app with 2 stores, but if you have 20 - this will matter.
  • Achilles
    Achilles about 5 years
    @ShevchenkoViktor How do you handle if you had a component which is being injected with bankAccountStore as per your example, and is rendered multiple times. And if the parent component needs to access all the values in bankAccountStore (which could have multiple bank account details ).
  • uneet7
    uneet7 almost 4 years
    Since this answer is getting lot of upvotes, please note that these approaches do not work with serializer if you want to use mobx in a SSR project
  • Hidayt Rahman
    Hidayt Rahman almost 3 years
    one of the clean solution
  • pjoshi
    pjoshi over 2 years
    Just in case someone looks for React Functional component use of this, i used it in following way - import React, { useEffect } from 'react'; import { inject, observer } from 'mobx-react'; const UserManagementScreen = ( props ) => { ` const { userStore } = props;` ` // Other State` ` return (` ` <>....</>` ` )` } export default inject('userStore') (observer(UserManagementScreen)); `