-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Mobx vs Reactive Stream Libraries (RxJS, Bacon, etc)
A common question is how does MobX relate to RxJS, Bacon.js, Kefir, etc...
First, it is important to realize that stream libraries, although similar, solve a different problem than Mobx. These are not competing, but rather complementing concepts. For example, RxJS helps you react to events while MobX will help you react to values (the state). As Andre Staltz argued and proved with Cycle.js, events and state are actually interchangeable.
So when should you pick events over state or vice versa?
Let's suppose you have a person object with three attributes (observable properties in MobX or streams in RxJs):
class Person {
constructor(firstname, lastname, nickname) {
this.firstname = firstname
this.lastname = lastname
this.nickname = nickname
}
}
Lets suppose you want to reactively derive a 'displayname' for the person. You want something like this...
displayname() {
return this.nickname ? this.nickname : this.firstname + ' ' + this.lastname
}
In MobX this would look like:
const displayname = computed(() => this.nickname ? this.nickname : this.firstname + ' ' + this.lastname)
In RxJS you would do something like this:
const displayStream = person.nickname.combineLatest(this.nickname, this.firstname, this.lastname)
.map([nickname, firstname, lastname] => nickname ? nickname : firstname + " " + lastname)
.distinctUntilChanged
These two observables have roughly the same behavior, and by roughly I mean that there are several differences because MobX applies transparent reactive programming. In MobX you hardly have operators, as stuff is normally combined through normal javascript constructions (there are two operators; for throttling and for self closing 'streams') In MobX you don't define the dependencies of the derivation, in RxJS you need to do this using combineLatest or any other operator.
MobX will automatically stop listening to observables that are not actively in use. So if the person has a nickname, the first- and lastname are not relevant for the outcome of the function and hence they won't be observed. This means that the derivation (or map function) won't be executed if the first name changes when using MobX, but in the RxJS case the map will be executed.
This is similar to the concept of Hot vs. Cold observables. MobX would be similar to that of Cold observables. This means that values are lazily evaluated on a subscription basis. They are only computed while an observer or subscriber is listening.
If the nickname was to be assigned a new, but not a different value, in MobX the derivation would not be executed either (as the result would be the same), but in RxJS it would be executed (which you could fix by introducing distinctUntilChanged on all streams). There are times when you may want a repeated value notification to react to, this is something outside of the scope of MobX and in such cases of values over time, reactive streams can accomplish this task.
There are some differences which are not displayed in this concrete example, but to name a few more: MobX will evaluate computations glitch free, something RxJS cannot do, streams that are in a diamond relation...
(x = a.combine(b), y = a.combine(c), z = x.combine(y))
will emit temporarily inconsistent values.
MobX has first class support for efficiently working with complex data structures like objects, arrays and maps. Now this might sound like RxJS is a bad thing, but it isn't! As said, the libs are complementary.
So, in summary, when should you use stream libraries over MobX? Answer: when time or history plays an important role. The above examples is where MobX shines; if you can derive it from the state you have now, transparent reactive programming is a lot easier and more consistent due to the transparent tracking and support for complex object models. You don't have to learn operators and dependencies are tracked automatically.
But if time plays an important role, like when throttling, accumulating events or having complex join patterns like zip, these are the cases where you want to work with streams.
MobX suffices and is easier and more efficient in 90% of the cases; where you want to derive something consistently from the latest state. But for the other 10% of use cases, reactive stream libraries really shine. This is usually when I/O plays an important role.
And surprisingly, there are cases when these two concepts can combine together really nicely.
For example, if you want to throttle the user input before updating the state and the rest of the app. You could accomplish this with a workflow like this:
DOM events -> RxJS -> Update state -> MobX -> Update UI
People have been using such a workflow and it's extremely powerful.
As shown in mobx-utils, connecting RxJS streams to MobX observables and vice versa is actually pretty straight forward!