MobX - Like React, but for Data
Catching up on blogged opinions about MobX and where it fits in (especially in relation to Redux), I see much confusion. There is a suspicion of it arising from fear of mutability. It has none of the frameworky ceremony of Redux, and that seems to cause anxiety in some.
Even its defenders seem a little apologetic, like MobX is okay despite the heresy of allowing data to be mutable and object-oriented. The great Basarat even humorously welcomed me to the dark side!
I'm fine with being on the edgy team. You'll usually find me in my leather jacket and shades, posing on my parked Harley Davidson and chewing on a matchstick, intimidating the townspeople. Why? I don't have to explain myself to you, lady.
But (try to keep this quiet, for the sake of my dangerous image) the truth is more reassuring. In fact MobX is based on some of the very same basic concepts that make React itself so fantastic. It implements those ideas in a way that is more seamless (reducing syntactic noise) and automatic (reducing opportunities for mistakes), while generalising them to cover a wider set of problems.
Increased power with reduced responsibility! What's not to love?
Immutability
In functional programming, most data is immutable. With the arrival of const
for local variables (but sadly not parameters) in JavaScript (along with readonly
for properties in TypeScript), most people soon realise that they use const
far more than let
and even try to organise their code so as to minimise the unnecessary use of let
.
But even hardcore FP languages recognise the practical need to work with changing data. The universe is stateful and it's a poor programming language that can't deal with such a fundamental aspect of reality. Hence Clojure provides four different ways to tame mutable values.
Even Haskell, that purest of pure FP languages, is in a sense two languages, one of which emulates imperative programming and enables a variety of ways to deal with mutable data.
It's not necessary, nor practical, to eliminate mutable data. But it is important to handle it carefully. What these solutions have in common is a special way of referring to a mutable value, so it can be managed. They have a way for an immutable object to store a kind of "handle" to a piece of mutable data, shielding its owner from the need to be mutable, but enabling the data to be accessed safely.
Derived data and partial updating
The render
function of a React component is a pure function that returns a tree of lightweight objects describing the desired UI. How pure is it? Very, even though it doesn't look that way at first because it has no formal parameters. There are strict rules about what data it can depend on: props and state. React manages these two things, and from the render
function's perspective they are treated like they are the immutable parameters to a pure function. React re-renders automatically if these parameters change. If render
depended on anything else that might change, the system breaks down.
Even so, there is a method called forceUpdate
available on any component, which means that it is possible to extend the scope of render
, causing a render whenever necessary. This means we can broaden our definition of "state" to include anything that is able to trigger a re-render of any component that depends on it. We call such things observables.
So, render
is a pure function of props and state, where state may be any data that has been properly set up to trigger a re-render when it changes; this includes React's own this.state
out-of-the-box, of course.
The beauty of this is how easy it is to code for. The downside is that if you prepared your entire UI using a single render
function, you might be repeating a lot of work (and throwing away a lot of temporary objects) every time the slightest thing changes in the state data.
But React has a solution for this, one that happily has other spin-off benefits: componentization. Each component is a bounded sub-application. The data depended on by a component may change, and cause that component to re-render, and the parent component is not affected at all.
[Sidebar: Unfortunately, this does not automatically benefit you if you rely on props. The way a component's props change is by the parent being re-rendered, so it passes different props to the child. To improve on this, React offers the overridable shouldComponentRender
, which is an opportunity for hand-written optimisation, and therefore an opportunity for hand-written bugs.]
The correctness of React's built-in this.state
is obvious: when you change the state with this.setState
, it triggers a re-render. The only problem is the tendency of relevant state data to be needed across multiple components.
So why not dissociate the state observable(s) from the components? That would introduce the problem of how/when to wire up the change notifications to the re-rendering of components. What you need, and what MobX takes care of, is for components to automatically detect which observables they depend on.
The point I want to make here is that render
generates some derived data (the virtual DOM tree it returns is mere data like any other), and a React component is a unit of memoization and re-evaluation. If you re-executed a single whole-app render
function in response to any state change, you would mostly be repeatedly computing the exact same stuff. But by dividing it up into little islands of cached results and only re-executing them when their specific inputs change, you greatly cut down the work to be done. This is why the React devtools include a neat feature that highlights the components that just refreshed.
In MobX, computed provides this exact same facility, but generalised to cover functions that generate any kind of data, not just virtual DOM trees. The function must be pure in the sense that it may only depend on the values of observables. A computed
property is like a component that "renders" data: a bounded unit of memoization and re-evaluation. It re-renders automatically when there is a change in any of the observables it depends on.
See? MobX isn't weird or dangerous. It's just like React itself. Just generalised and more powerful. It's React for data.
Observables
Let's be precise about what an observable is. The this.state
feature of React is a disguised instance of the observable pattern.
An observable is an object that stores a single value. You can get
or set
the current value, and you can listen
to it so your callback will be executed whenever the value changes. Also you can quit listening (very important). That's it.
In a React component, the get
function is represented by this.state
, and the set
function becomes this.setState
. But there is no public listen
function. Instead React assumes that only the component's own render
function (or rather, the built-in logic that calls render
) needs to be listening, and so it sets that up (and tears it down) for you.
Redux also contains this pattern. A Redux store is an observable. It has subscribe
and getState
functions. Rather than a raw setState
it has an associated reducer function that defines the action-based API by which you can update the state (although there's nothing stopping you from including a SET_STATE
action in that API, and this is often the simplest way to initialise the state).
How many observables? When to listen?
Although it's not enforced at all, Redux also has a rule: there should be only one such observable for the entire app. This is obviously open to interpretation: what are the boundaries of an app? What if you're building a reusable component? It's potentially more of a guideline than a rule, but it is an important part of Redux nonetheless.
Redux's "single store" rule, and Reacts's own built-in subscription approach, help address the one tricky aspect of the observable pattern: how do you know when to listen and (just as importantly) stop listening? Anywhere that you get
the value of an observable, you are presumably going to use that value to generate some other data derived from it. So you better be doing this in some context in which you are listening to the observable, or else the derived data will be out of date as soon as the observable's value changes. And then you better figure out when it's time to stop listening, or you'll cause memory leaks and unnecessary background computation.
React solves this problem by building an observable into each component and tying the subscription to the component's lifecycle.
Redux does it by having one observable, so at least you don't have to figure out which one to listen to.
MobX does it by automating the entire thing. Merely getting the current value of an observable is sufficient to listen to it (although only in relevant contexts: in the render
function of an observer component, or in the body of a computed
property).
Mutability via immutability
Suppose you have a Person
type and you need to store an instance of it as part of your UI state. Roughly:
interface Person {
firstName: string;
lastName: string;
}
Option 1 is to store it in a single observable, and update it by creating a whole new Person
instance whenever you need to change either field. So the type can become:
interface Person {
readonly firstName: string;
readonly lastName: string;
}
And you can update the firstName
of person1
like this:
person1.set({ ...person1.get(), firstName: "Homer" });
Option 2 is to always hold the same instance of Person
, which has been tweaked thus:
interface Person {
readonly firstName: Observable<string>;
readonly lastName: Observable<string>;
}
Now to update firstName
, you say:
person1.firstName.set("Homer");
Note that in both cases, Person
is at least shallow-immutable; its properties are readonly
. The person itself is never modified. In option 2 we change the observables, not the Person
that owns them (nor the string objects they contain: they are replaced with different strings). Observables act as a bridge to the immutable world.
Important: Everything else, apart from the one value stored in an observable, is immutable. The thing that owns the observable is immutable; it always owns the same immutable. The thing stored in the observable is immutable. This is still true even if the immutable stores a simple string or number: changing the value means stuffing a different string or number into the observable.
Of course, in modern JS runtimes (available in practically all browsers now in use) we can define a property with custom get/set functions. This means that we can make an object with observable properties but go back to the original interface:
interface Person {
firstName: string;
lastName: string;
}
And then we can update firstName
like this:
person1.firstName = "Homer";
This makes it extremely familiar and user-friendly. But from a theoretical standpoint it's important to realise that this is only a syntactic shift. We are still not modifying a field in a Person
! It just looks like we are. The Person
owns an observable, always the same one: Person
is immutable. We are just stuffing a different string into the observable.
Clearly there is little difference between any of these options in terms of immediate capabilities. It's more a practical matter of which things you want to make easy and fast. The key point here is that mutable data is ultimately unavoidable, but this is not a disaster as long as you have some consistent pattern for referring to it, holding it at arms length, and thus mitigating the problems it can cause.
Immutability is a tool that plays a vital role, but is means to an end, not the end itself. It has to coexist with mutability somehow.
Postscript: Knockout.JS
If you need any more convincing of the middle-of-the-road-ness, safety and reliability of the concepts in MobX, look no further than Knockout.JS. From its first version in summer 2010 it had observable
, computed
(originally called dependentObservables) and automatic dependency tracking between them. All are functionally identical to the equivalents in MobX.
KO is almost ridiculously stable. Even the website has barely changed in over five years! Google's Angular 1.x arose and fell in that time, while the core concepts of KO stayed the same, and they live on now in MobX.
See also
computed-async-mobx - a library I just published that extends the power of computed
to expressions that return promises, integration asynchronous data into your nice pure MobX declarations.