Mobx is very unopinionated about how you structure your app, which is ultimately a great thing, but it can be intimidating when you are building something bigger. There are some recommendations but they don’t go into much detail beyond simply separating out UI related logic from business logic. As your UI grows you’ll probably want to figure out a more structured approach. Here’s what I’ve found to work best.
This is where all the business logic for your app goes. How you structure this is up to you. This store should be completely decoupled from the UI to the point where if you were to rewrite your app using some other framework, or to write it as a mobile app, you could reuse this with no changes. Typically this will include things like API calls or state that is independent of the UI, like information about the logged in user.
Let’s say you’re building YouTube. As of today, it looks like this:
The page has a header bar, a sidebar navigation menu, and an area for the current page content. All of these will have some kind of logic and state associated with them that we want to put into stores, but figuring out how these stores should be organized can be hard.
When you navigate through the app, the header and nav stay constant and the current page content updates. As a result, the header and nav shouldn’t depend on anything about the current page, since it can change. On the other hand, the page can know that it will be rendered into the layout, so the stores for the page can know about the stores for the header and nav. Similarly, all of the stores can know that they are part of the app, and should have access to the app store. In this case, we might build our UI stores like this:
Each of the stores in the left column is responsible for coordinating access between their respective stores on the right. The stores on the right are responsible for the actual behavior of your UI and will contain most of your logic.
As the user navigates through the app, the current page store will be destroyed and replaced with an instance corresponding to the new page. To get a better idea of how this is implemented, let’s look at how the page store gets created:
When our page component is rendered, it creates it’s own store and puts it into a provider so that anything below it can access it. We also inject all the stores from any providers above and pass them to the constructor for the page store. The page store itself will then create any stores that are needed by the page and is responsible for providing any dependencies to them. When the user navigates, this component is unmounted (by whichever routing library you use), and a new page is mounted, creating a new page store. In this case, our page store might look like this:
The page store instantiates the other stores by passing in a callback which generates the props that those stores need. This allows us to reuse these stores in other contexts, and using a callback means these props can be reactive to changes. In this case, the VideosStore gets access to the videos index api, but the VideosStore itself doesn’t have to know about the implementation, making it easy to test. Since all of our stores will need to handle the props coming in as a function, let’s make a base class for our stores that can handle this behavior for us:
Now we can create our VideosStore by subclassing the BaseStore and accessing
this.props, just like a React component:
Accessing Props from Components
Sometimes our stores correspond 1:1 with a component on the page, and need access to props from that component that are not available in the store. For example, if our page url was parameterized, like
/videos/:videoId, our page store would need access to this
videoId to know which content to load. This information should be provided as a prop to your component by your routing library. In this case, we want be able to access the component props in the store. We can update our BaseStore to include this functionality:
Using this new BaseStore, our component can bind itself to the store by calling
this.state.store.bindComponent(this) and the store will then have access to the combined props through
this.props. Any props coming from the component will override the props passed in to the store directly. This behavior is similar to that of inject from mobx-react.
In this case, our stores formed 3 layers: the app store; the layout store; and the page store. In your case, it may make sense to have more or less. Typically whenever you are using a router to determine what to render, that will constitute a new layer of stores. Grouping your stores into layers helps to avoid dependencies on stores which may not behave as expected, or even exist.
By having a parent store at each layer that is responsible for setting up all the other stores, we can make those stores easier to test and reuse. All the UI stores expect to work with a plain props object, just like a React component, while the parent store is responsible for deciding what these props actually correspond to. Since the parent store has relatively little logic in it, we can reduce the amount of testing that needs to be done there and focus more on unit testing the individual parts.