Introducing mobx-state-router
Make UI a function of your state
mobx-state-router is a MobX-powered router for React apps. It decouples state from the UI, which has the following benefits:
- UI is no longer responsible for fetching data. Data is fetched during state transitions using router hooks.
- UI becomes a function of the state. It only needs to render data and trigger state changes based on user input.
- The router can override routing requests based on the application state. For example, it can redirect to the Sign In page if the user is not logged in.
I’ve created a demo app called MobX Shop to show mobx-state-router in action. I’d love for you to give it a try and give your support to make this the go-to router for MobX. Feedback and PRs are welcome!
Motivation: Decouple State from UI
The motivation to write mobx-state-router came from the frustration of dealing with subtle bugs in my code resulting from fetching data in componentDidMount()
. Moreover, why should a component be responsible for fetching data in addition to rendering it? It feels like a violation of the Single Responsibility Principle. Looking for a better way, I came across this article by Michel Weststrate–How to decouple state and UI. Here's an excerpt from the article which clearly describes the root cause of my problems:
I discovered that most React applications are not driven by the state that is stored in stores; they are also driven by the logic in mounting components.
- Interpreting route changes is often done in components; especially when using react-router. The router constructs a component tree based on the current URL, which fires off componentWillMount handlers that will interpret parameters and update the state accordingly.
- Data fetching is often triggered by the fact that a component is about to be rendered, and kicked off by the componentWillMount lifecycle hook.
Michel then shows how to decouple state and UI, resulting in a robust architecture where components don’t have to fetch data. They are purely a function of the application state stored in stores. Stores become more like a state machine, making it easy to follow the transitions of our application. mobx-state-router provides a first-class implementation of this idea.
My hope is that mobx-state-router will allow developers around the world to create more robust React applications with less headaches. Let’s take a deeper look at how it works.
Concepts
Router State
At the heart of it, mobx-state-router provides a RouterStore
that stores the RouterState
.
RouterState
consists of 3 properties:
routeName
: A string that defines the state of the router. For example, the"department"
state in MobX Shop signifies that we are displaying a department.params
: A set of key-value pairs that enhances the state. For example,{id: "electronics"}
may signify that we are not only in the "department" state, but specifically in the "electronics" department.queryParams
: A second set of key-value pairs to further enhance the state. For example,{q: "apple"}
may signify that we want to query for the string "apple".
As you may have guessed, this structure facilitates the decomposition of a URL into separate parts. However, from the router’s viewpoint, this is simply application state that can be manipulated without any concerns about who is manipulating it (whether it’s UI components or some other logic).
Now that we understand RouterState
, let's move to the top-left of the diagram. The HistoryAdapter
, which is responsible for translating the URL in the browser address bar to the router state and vice-versa. It is essentially an "observer" of the address bar and the router state.
Moving to the top-right, the RouterView
watches the router state and instantiates the associated UI component. Finally, the UI components themselves can change the router state in reaction to user actions, such as keyboard entries and mouse clicks.
Fetching Data
As mentioned before, UI should not be responsible for fetching data. This means no data fetching in componentWillMount()
or componentDidMount()
. mobx-state-router facilitates data fetching during state transitions using the onEnter
hook. This hook is called just before a new router state is entered and is the perfect place to kick off a data fetch. Here's an example from MobX Shop:
{
name: 'department',
pattern: '/departments/:id',
onEnter: (fromState, toState, routerStore) => {
const { rootStore: { itemStore } } = routerStore;
itemStore.loadDepartmentItems(toState.params.id);
return Promise.resolve();
}
},
This code is part of route definitions. We define an onEnter
hook that calls itemStore.loadDepartmentItems()
to kick off the fetching process. It does not have to wait for the fetch to complete. The itemStore
maintains an isLoading
flag to indicate the status of the fetch. The last line in the onEnter
hook resolves the promise to let the router proceed to toState
.
Redirecting to the Sign In page
If the user is not logged in, we can redirect them to a Sign In page. Not only that, we can redirect them back to the requested page on a successful sign in. For example, in MobX Shop, we allow the user to add items to the shopping cart without having them signed in. However they can’t proceed to checkout unless they are signed in. This is achieved by using the beforeEnter
hook in the route configuration. Here's the code from MobX Shop:
{
name: 'checkout',
pattern: '/checkout',
beforeEnter: checkForUserSignedIn
}
checkForUserSignedIn()
is a shared function used by multiple routes. It is defined in the routes.js file:
const checkForUserSignedIn = (fromState, toState, routerStore) => {
const { rootStore: { authStore } } = routerStore;
if (authStore.user) {
return Promise.resolve();
} else {
authStore.setSignInRedirect(toState);
return Promise.reject(new RouterState('signin'));
}
};
This function allows the router to proceed if the user is already signed in. If not, the requested state is saved in the authStore
and the app is redirected to the signin
state. On a successful sign in, authStore
redirects the app to the originally requested state. Here's the code from MobX Shop:
@action
setUser(user) {
this.user = user;
this.rootStore.routerStore.goTo(this.signInRedirect);
}
TL;DR
mobx-state-router decouples state from UI.
- The state-driven approach keeps data-fetching functions within the router.
- The UI is now less complicated — all it needs to do is to render data and change state based on user input. Everything else happens magically!
Experience the magic yourself — try out the mobx-state-router.