A few weeks ago Bertalan Miklos wrote a very interesting blog in which he compared the proxy based NX-framework with MobX. That blog is not only interesting because it proves the viability of proxies, but even more because it touches some very fundamental concepts in MobX and transparent reactivity (automatic reactivity) in general. Concepts I probably did not elaborate enough on so far. So let me share the mental model behind some of the unique features of MobX.

Why MobX runs all derivations synchronously

The article touches a very remarkable feature (imho) of MobX: in MobX all derivations are run synchronously. Which is quite unusual. Most UI frameworks don’t do this (if any at all). (Reactive stream libraries like RxJS run by default synchronous as well, but they lack transparent tracking so the situation is not entirely comparable).

Before starting MobX, I did quite some research on how existing libraries were perceived by developers. Frameworks like Meteor, Knockout, Angular, Ember and Vue all expose reactive behavior similar to MobX. And they exist already for a long time. So why did I built MobX? When digging through the issues and comments of people that were unhappy with these libraries, it occurred to me that there was one recurring theme which caused the chasm between the promise of reactivity and the nasty issues one had to deal with in practice.

That recurring theme is “predictability”. If a framework runs your code twice, or with a delay, it becomes hard to debug it. Or to reason about it. Even ‘simple’ abstractions like promises are notoriously hard to debug due to their async nature.

I believe unpredictability to be, rightfully, one of the most important reasons for the popularity of Flux patterns and especially Redux: it addresses exactly this issue of predictability when scaling up. There is no magic scheduler at work.

MobX however took another approach; instead of leaving behind the whole concept of automatically tracking and running functions, it tries to address the root causes. So that we can still reap the benefits of this model. Transparent reactivity is declarative, high-level and concise. It does this by adding two constraints:

Make sure that for any given set of mutations, any affected derivation will run exactly once. Derivations are never stale, and their effects are immediately visible to any observer.

Constraint 1. addresses so called “double runs”. It makes sure that if one derived value depends on another derived value, these derivations run in the right order. So that none of them accidentally read a stale value. How that exactly works is described in great detail in an earlier blog post.

The second constraint; derivations are never stale; is a lot more interesting. Not only does it prevent so called “glitches” (temporary inconsistencies). It requires a fundamental different approach to scheduling derivations.

So far UI libraries have always taken the easy way out when it comes to scheduling derivations: Mark derivations as dirty, and re-run them on the next tick after all the state has been updated.

This is simple and straight forward. It is a fine approach if your only concern is updating the DOM. The DOM usually lags a bit ‘behind’ and we hardly read data from it programmatically. So temporary staleness is not really an issue. Yet temporary staleness kills the general applicability of a reactive library. Take for example the following snippet:

const user = observable({

firstName: “Michel”,

lastName: “Weststrate”, // MobX computed attribute

fullName: computed(function() {

return this.firstName + " " + this.lastName

})

}) user.lastName = “Vaillant”

sendLetterToUser(user)

The interesting question now is: When the sendLetterToUser(user) runs, will it see an updated or a stale version of the fullName of the user? When using the MobX the answer is always “updated”: Because MobX guarantees that any derivation is updated synchronously. This does not only prevent a lot of nasty surprises, it also makes debugging much simpler as a derivation always has the causing mutation in its stack.

So if you are wondering why a derivation is running, simply walking up the stack brings you back to the action that caused the derivation to be invalidated. If MobX would be using async scheduling / processing of derivations these advantages are lost. The library would not be as generally applicable as it is know.

When I started on MobX, there was a lot of skepticism on whether this could be done efficiently enough: Ordering the derivation tree and running derivations with each mutation.

But here we are, with a system that is out of the box often more efficient than manually optimized code, as reported for example here and here.

Transactions & Actions

Yes, there is a small payoff to pay; mutations should be grouped in transactions to process multiple changes atomically. Transactions postpone the execution of derivations to the end of the transaction, but still runs them synchronously. Even cooler; if you use a computed value before the transaction has ended, MobX will ensure you get an updated value of that derivation nonetheless!

In practice nobody uses transactions explicitly, they are even deprecated in MobX 3. Because actions apply transaction automatically. Actions are conceptually much nicer; an action indicates a function that will update state. They are the inverse of reactions, which respond to state updates.

The conceptual relation between actions, state, computed values and reactions

Computed values and reactions

Another thing MobX focuses strongly on is the separation between values that can be derived (computed values), and side effects that should be triggered automatically if state changes (reactions). The separation of these concepts is very important and fundamental to MobX.

Example derivation graph. Observable state(blue), computed values (green) and reactions (red). Computed values will suspend (light green) if not observed (indirectly) by a reaction. MobX makes sure that each derivation runs only once and in the most optimal order after a mutation.

Computed values should always be preferred over reactions.

For several reasons:

They provide a lot of conceptual clarity. Computed values should always expressed purely in terms of other observable values. This results in a clean derivation graph of computations, rather then an unclear chain of reactions that trigger each other. In other words, reaction that trigger more reactions, or reactions that update state: They are both considered anti patterns in MobX. Chained reactions lead to a hard to follow chain of events and should be avoided. For computed values MobX can be determine whether they are in use somewhere. This means that computed values can automatically be suspended and garbage collected. This saves tons of boilerplate and has a significant positive impact on performance. Computed values are enforced to be side-effect free. Because computed values are not allowed to have side effects, MobX can safely reorder the execution order of computed values to guarantee a minimal amount of re-runs. It can decide to only lazily run the computation if it is not observed. Computed values are cached automatically. This means that reading a computed value will not re-run the computation as long as none of the involved observables did changes.

In the end, every software system needs side effects. For the simple reason we always need to bridge from reactive to imperative code. For example to make network requests or flush the DOM. However, often these reactions can be put away in clean abstractions like React observer components.

So MobX goes great lengths to make sure stale values can never be observed, and derivations do not run more often then one would expect. In fact, computations are not even run at all if nobody is actively observing. In practice this makes all the difference. There is often some initial resistance to MobX, as the concepts remind people of the unpredictable behavior of MVVM frameworks. However, I believe the semantical clarity of actions, computed values and reactions, the fact that no stale values can be observed, the fact that all derivations run on the same stack as the causing action, makes all the difference here.