MobX is not a framework

I have been using MobX at work for almost a year now, and I’m beginning to form my view on what it is, how it can be used and how it should be used.

Here is a quick description of MobX in my words: MobX offers a simple way to detect changes and run side effects when JavaScript objects are mutated. One such side effect can be to render a react component.

When I joined my current team, the application structure was about 10 singleton MobX-stores representing different domain objects and some react components that imported the singletons, rendered their data and called their actions. A singleton is created by exporting an instance instead of a class:

export default new Store();
view raw singleton.ts hosted with ❤ by GitHub

The stores imported each other, and components imported multiple stores. It was fairly simple, but not particularly structured and hard to test. As a fan of functional programming concepts, I thought the contrast to things I am used to, like flux architecture, immutability and pure functions was quite sharp.

A few problems with using singletons were

  • Not straightforward to mock.
  • Implicit instantiation of stores at import time. Not clear which order stores are created in.
  • Many imports in many files.
  • Only one instance per runtime, we had to manually reset the state of all stores between tests.

Since then we have moved to the recommended root store pattern, where a root store instantiates all other stores and injects their dependencies along with a reference to the root store itself so stores can access each other via the root store. Instead of importing several singletons, it’s recommended that the root store is provided to components via a React Context. This basically solves the above problems.

That is great! But still I can’t help but think that the contact area with MobX is much larger than it needs to be.

Basically my question is: do we actually need to know about the fact that we are using MobX as we add features to an app?

From detecting changes in objects, to domain stores and the root store pattern, is quite a leap. Sure, the docs make clear that this is only a recommended way of coding, but what is really the developers’ minimum contact area with MobX? 

The absolute minimum should be one store class with one observable and one action. It turns out to be quite feasible.

With only one observable and one action, it sounds like our store needs to be very generic. For the observable, just put all state in one observable and call it state – not that radical. But how do we manage with only one action? Well, basically by passing a function and some extra arguments to the action. The action passes the current state and extra arguments to the function, and the function returns the new state. The passed function itself does not have to be a MobX action, since it is invoked inside the action of the store. 

swap(swapFunction: (state: T,rest: any[]): T,args: any[]): T {
this.state = swapFunction.apply(null, [this.state,args]);
return this.state;
view raw swap.ts hosted with ❤ by GitHub

The real benefit here is that the function can be a pure* JavaScript function without any knowledge of MobX. Logic is now decoupled from MobX, and all we expect to grow is our library of pure functions and components.

* Pure in the sense that the function only depends on its parameters. For the purpose of this post, mutating the input is fine as long as it is returned.

Let’s call this store class Bucket, the observable variable state, and the action swap. (Yes – very inspired by Clojures’ atom.)

What about computed values? I will just quote the MobX docs:

“It is important to realize that computed values are only caching points. If the derivations are pure (and they should be), having a getter or function without computed doesn’t change the behavior, it is just slightly less efficient.”

And besides, computed values don’t accept parameters anyway. If you really need them, just extend Bucket and add computed getters. The computed getters can still call pure functions of the state and return the computed value.

What about asynchronous actions? Async code does not have to, and should not be garbled together with state updates (swaps). For example, a service call should be a side effect of a swap. When the response is received, the state is just updated with swap.

Where should side effects happen? To mimic Clojures’ add-watch, we can let users of Bucket register callbacks for when the state changes, using MobX’s autorun:

onChange(listener: (state: T) => void) {
autorun(() => listener(this.state));
view raw onChange.ts hosted with ❤ by GitHub

This makes it simple to perform side effects when the state indicates so. The listener typically checks whether some side effect should be performed, and since listener called in a reactive context, it will be rerun each time any accessed properties change.

What about domain object stores? Since Bucket is completely generic, you just create more instances of Bucket for your domain objects. Although your logic will not live in a store, but in a library of pure functions, it could make sense to split the state by domain into several buckets. If using TypeScript i would suggest making Bucket a generic class.

How will components get access to the store? Nothing that has been said so far prevents us from using React Context to provide the components with a Bucket instance, I just think it’s overkill. This is a JavaScript problem, not a React problem. Why not use plain old JavaScript closures to bind a component to a store with a maker function? Yes you have to do it everywhere a component uses Bucket, but it is very simple.

What about performance? Intuitively, performance should not be affected by coding this way, with the possible exception of computed getters as discussed earlier. But for the fun of it, I forked the js-framework-benchmark and added an implementation. The results show no significant difference from comparable frameworks.

Finally, let’s look at the MobX authors’ view of the role of a store:

The main responsibility of stores is to move logic and state out of your components into a standalone testable unit that can be used in both frontend and backend JavaScript.

The suggested responsibility is indeed better than having logic and state in components, but i think we can do even better. Coding in the way I have discussed:

  • Decouples logic from MobX, allowing MobX to become a detail rather than a concept of your app, while still leveraging MobX’s power.
  • Simplifies testing.
  • Moves emphasis from OO to FP.
  • Lets developers focus on JavaScrip instead of frameworks and libraries.
  • Does not affect performance.

While exploring these ideas I created a repo proving some of the concepts. I’d especially recommend to have a look at the test files.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s