MobX and Redux give you two opposing ways to manage your state effectively in a React application. As an experiment I attempted to merge the good parts of both approaches, and ended up with a new library, mobdux.
You can watch a video briefly explaining the inspiration behind the library here:
In this article I’ll be outlining the thought process and some problems with the initial approach that led to the inception of mobdux. Let’s start by breaking down the good and bad parts of each library.
- Very explicit
- Dumb components have smart wrappers that take minimal props (usually an identifier or nothing) and look up all the props needed by the dumb component.
- Boilerplate (making a counter increment involves writing: a selector, an action-type an action-creator and a reducer)
- Every state change reconciles every smart component (i.e at the very least every smart component will have its mapStateToProps function re-run)
- Selectors are an optimisation, not a first class citizen
- Simple and concise, minimal boilerplate necessary.
- Minimal re-renders out of the box (observers are smart enough to know when they should re-render themselves based on the props they use).
- Works really well with typescript as MobX is written in typescript so there’s no need to rely on third party type definitions.
- Relies on the magic of observables and observers.
- Leaks knowledge of MobX into components through observer decorators. All components become coupled to MobX by taking observables or stores as props.
- No dumb/smart component separation; props are often broken up and passed down the component hierarchy.
It is possible (and even recommended) to follow a similar state structure in both libraries, in that your entire application state should exist in one place. In Redux, you get this for free as your state must be represented as one large, serialisable blob. In MobX it is easy to structure your stores in a tree-like manner so that at any point you can view the entire state of your application. In fact, the author is currently working on a library that enforces just that. Additionally, by using MobX in strict mode, you are forced to make any mutations to your state in actions. The combination of a single store, strict mode, and the excellent mobx-react-devtools help give back some of the predictability and traceability of Redux.
One of the ways the libraries significantly diverge however, is when connecting your state to your components. MobX is largely unopinionated about how this is done. Most of the documentation points consumers in the direction of just decorating all of their components as observers and passing observables down the component hierarchy through props. Redux on the other hand, recommends the smart component / dumb component pairing so that each component (regardless of where it sits in the hierarchy) can look up everything it needs to render itself.
I prefer the Redux approach for a number of reasons:
- Easier to move things around in your component hierarchy.
- No pass-through props, where a component forwards props to its children despite not needing them for its own rendering.
- Easier to test / re-use / write storybook stories for the dumb components that are only interested in rendering.
- Dumb components are completely decoupled from Redux, allowing for re-usability if the consumer decides to change state frameworks (to MobX or relay for example).
What’s more interesting is that the mapper function passed to inject is also tracked by MobX. This means it will only ever be re-run if MobX already knows that it should be re-run based on the observables and computed properties that are referenced in the implementation. This gives inject an advantage over connect from react-redux. It ensures that when the state changes only the components interested in the change will have their mapping function re-run. This completely removes the need for memoized selectors whilst still guaranteeing us minimal re-renders for free! Also, we can remove all knowledge of MobX from our dumb components by encapsulating all of this logic in our smart component wrapper, removing the need for the observer decorator entirely.
Using inject to separate smart and dumb components
By combining a single MobX store with heavy use of the inject method, we end up with the following architecture:
- Keeps the simplicity and conciseness of MobX.
- Minimal re-renders out of the box.
- Maintains the smart/dumb component separation.
- No need for the observer decorator.
- No need for memoized selectors (just a mapping function per component).
- Smart components only reconcile when they should (not on every state change).
- Still relies on the MobX magic.
- Referencing properties nested within a computed property in a mapping function can cause unnecessary reconciliation (and re-renders).
- Directly returning an observable array in a mapping function will prevent the component from reconciling (or re-rendering) when the array changes.
- We want a normalised map of entity data so we can easily pass ids around for our smart components and actions, but MobX prefers to work with denormalised data.
Points 2 and 3 can be solved by using a thin wrapper around the inject method that unboxes observables and does a shallow equality check before re-rendering. Point 4 is due to a current limitation in the MobX library that will be solved when observable maps are implemented using proxies. In the mean-time, we can simply use computed properties to transform our denormalised source of truth into a normalised form for use in our mapping functions and actions.
With this architecture in mind, I’ve implemented a library of simple utilities (that I’m affectionately referring to as mobdux) that make working with this proposed architecture a breeze.
Here’s a brief example of a mobdux application that maintains a list of independent counters. Note the complete lack of boilerplate, and the minimal passing of props!
And that’s it! If you want to give it a try, you can find the library here and its source here. You can also find the complete ‘counter list’ example in typescript here. For comparison’s sake, you can also find the ‘counter-list’ example implemented in Redux here.