MobX 3 released: Unpeeling the onion

I’m proud to announce that MobX 3 is generally available! Version 3 doesn’t introduce many new concepts, nor did barely anything change in the core algorithm. But the API has received many improvements. The API is now more coherent, and it paves the path towards a Proxy based Mobx implementation (to be made once all major web browsers support this move). The full details of all the breaking and non-breaking changes can be found in the changelog, but read on for the gist of it.

Oh, and the homepage of MobX is now officially https://mobx.js.org!

Pro tip: If you want to migrate quickly, use a strongly typed environment (TypeScript recommended) so that the compiler can assist :-).


Unpeeling `observable` the onion

The most important change in MobX 3 is how observable data structures are created. The MobX 2 API has quite a few irregularities and edge cases. It was a community effort to redesign the observable API. In the longest thread in the issue tracker so far, many use cases and usage patterns were discussed. Many proposals were shot down in mid air, but I am very happy with the end result .

The new API follows the “onion pattern”. The API is now nicely layered; each API layer can be easily peeled back to reveal lower level functions. Let’s quickly walk through it.

The observable function / decorator works largely the same as in MobX 2. There are 3 notable changes:

  1. Objects are by default no longer enhanced but cloned. This is consistent with arrays and maps, and paves the path to Proxies.
  2. ES6 Map support has been added (string based keys only).
  3. Argumentless function values are no longer automatically converted into computed properties. This should avoid a lot of confusion.

Feel free to skip the remainder of this section if you are not experienced with MobX; it’s quite detailed…

Observable

The observable(data) function constructs new observable collections. Object, maps, arrays or boxed observables. If we unpeel observable, you will find that it just calls one of the following methods: observable.object, observable.map, observable.array and observable.box. Feel free to use those methods directly instead of the generic observable method.

Collection factories

Each of these collection types have the same semantics: If you assign a non-observable, primitive value to them, MobX will automatically clone and convert that object into an observable. By calling observable(newValue) before storing that new value. This enables deep observability by default. This recursive process can be described as making data deep observable.

Shallow collection factories

However, sometimes you want a collection where MobX doesn’t try to enhance the data into an observable. For example when storing JSX or DOM elements, or objects that are managed by an external library. In other words, sometimes you just need a shallow collection. A collection that itself is observable, but the values it stores are not. For this purpose MobX now also exposes functions to create shallow collections: observable.shallowObject, observable.shallowMap, observable.shallowArray and observable.shallowBox. In addition, similar to extendObservable, there is now extendShallowObservable.

Decorators & modifiers

If you are using decorators, the @observable decorator applies the deep strategy. You can make this more explicit by actually using the @observable.deep decorator. Similarly there is the @observable.shallow decorator, which converts any value you assign to it into a shallow collection (only arrays and maps are supported). And finally there is @observable.ref, which leaves any value you assign to the property completely as is, and just creates an observable references to the value you assign to it (similar to observable.shallowBox)

If you are not using decorators, you can still use those strategies when you create observable objects by calling these decorators as function. For example: const message = observable({ author: observable.ref(null) }). These new modifiers replace the old asFlat, asMap etc.. modifiers. And, as you might have guessed by now, with this we arrived at the core of the onion. These modifiers are used internally by the shallow and deep observable collections.


Error handling

MobX 3 introduces clear error handling behavior. Where MobX 2 did a best effort to recover from any exceptions thrown in a derivation, the semantics are now clearly defined:

Computed values will now always catch exceptions in derivations, and re-throw them to any consumer that tries to read the value.

Reactions will always catch exceptions, and by default just logs them to the console. This ensures that if one reaction throws an exception, this doesn’t influence / prevent the execution of other reactions.

Both computed values and reactions will now always recover from exceptions; that is, even though an exception is thrown, the tracking information is preserved. This means that they will continue to run and they will recover if the cause of their exception is removed. It is possible to attach custom error handling behavior to reactions by attaching an onError handler. It is also possible to attach a global error handler, which is great monitoring and testing purposes.

Further notable changes

MobX 3 now ships with Flow typings! Note that the flow typings do not cover everything, as some overload patterns can be expressed in TypeScript, but (afaik) not in Flow.

It is now possible to create actions that automatically bind to the correct this, similar to autobind. You can use action.bound(fn) or @action.bound instead of just action(fn) to achieve this.

The API of reaction and computed has been simplified and both take option objects now, instead of a multitude of parameters. Check the changelog for the details.

The callbacks passed toobserve are now always invoked with a change object. In MobX 2 the observe listeners of computed values and boxed observables were invoked with (newValue: T, oldValue: T), this has changed to (change: { newValue: T, oldValue: T }) which is consistent with spy, intercept and observe for collections.

Structurally comparison observables has been removed from the core. This feature was hardly used, and if it was used, it was often in places where boxed / reference observables would be the more appropriate solution, such as when working with immutable objects. It is still possible to create structural comparing computations ( (@)computed.struct) and reactions (which has an option for this).

transaction has been deprecated in favor of runInAction, which achieves the same.


All in all, probably nothing too exciting or new, but a lot of small improvements that make the API a tad more consistent. Migrating shouldn’t be too hard. Especially if you are not using modifiers yet, it should be straight forward. I’m quite proud that after this release the amount of open issues is down to roughly 25! Which I think is remarkably low for a project that is as complex and popular as MobX!

For this, I want to thank all the people that respond to the new issues on Github and in the Gitter channel. Your responses to beginner’s questions and input in design discussions is invaluable! Big shout out to @agambit @andykog, @benjamingr, @capaj, @jabx, @mattruby, @strate, @spion, @urugator and many others!

Remember, the full changelog with all details can be found here!


And now… back to mobx-state-tree. It is becoming awesome (with big contributions and awesome ideas from @mattiamanzati). It has a conference talk, 200 stargazers… but no real release yet :). So stay tuned if you are interested in an opinionated, reactive state container!

\* these are GitHub handles, not twitter ;-)

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.