In the previous part we looked at how you can setup a MobX state tree and make it observable. With that in place, the next step is to start reacting to changes. Frankly this is where the fun begins!
MobX guarantees that whenever there is a change in your reactive data-graph, the parts that are dependent on the observable
properties are automatically synced up. This means you can now focus on reacting to changes and causing side-effects rather than worrying about data synchronization.
Let’s dig in and see the various ways in which you can cause side-effects.
@action
as an entry pointBy default when you modify observables, MobX will detect and keep other depending observables in sync. This happens synchronously. However there may be times when you want to modify multiple observables in the same method. This can result in several notifications being fired and may even slow down your app.
A better way to do this is to wrap the method you are invoking in an action()
. This creates a transaction boundary around your method and all affected observables will be kept in sync after your action executes. Note that this delayed notification only works for observables in the current function scope. If you have async actions which modify more observables, you will have to wrap them in runInAction()
.
class Person {
@observable
firstName;
@observable
lastName;
// Because we wrapped this method in @action, fullName will change only after changeName() finishes execution
@action
changeName(first, last) {
this.firstName = first;
this.lastName = last;
}
@computed
get fullName() {
return `${this.firstName}, ${this.lastName}`;
}
}
const p = new Person();
p.changeName('Pavan', 'Podila');
Actions are the entry points into mutating your stores. By using actions, you can make updates to multiple observables into an atomic operation.
Tip As far as possible avoid directly manipulating observables from the outside and expose @action
methods that do this change for you. In fact, you can make this mandatory by setting useStrict(true)
.
autorun
to trigger side-effectsMobX ensures that the graph of observables always stays consistent. But if the world were only about observables, it would be no fun. We need their counterparts: the observers to make things interesting.
In fact, the UI is a glorified observer of your MobX Stores. With mobx-react you get a binding library that makes your React Components observe the store and auto-render themselves whenever the stores change.
However, the UI is not the only observer in your system. You can add many more observers to your stores to do a variety of interesting things. A very basic observer could be a console-logger that simply logs the current value to the console when an observable changes.
With autorun
we can setup these observers very easily. The quickest way is to supply a function to autorun
. Whatever observables you use inside this function are automatically tracked by MobX. Whenever they change, your function will re-execute (aka autorun)!
class Person {
@observable
firstName = 'None';
@observable
lastName = 'None';
constructor() {
// A simple console-logger
autorun(() => {
console.log(`Name changed: ${this.firstName}, ${this.lastName}`);
});
// This will cause an autorun() as well
// (thanks to Adam Skinner for pointing out)
this.firstName = 'Mob';
// autorun() yet again
this.lastName = 'X';
}
}
// Will log: Name changed: None, None
// Will log: Name changed: Mob, None
// Will log: Name changed: Mob, X
As you can see in the log above, autorun
will run immediately and also everytime there is a change to the tracked observables. What if you don’t want to run immediately, and instead run only when there is a change? Well, you have reaction
just for that. Read on.
reaction
to trigger side-effects after first changereaction
s provide more fine grained control compared to autorun
. First, they don’t run immediately and wait for the first change to the tracked observables. Also the API is slightly different than autorun
. In the simplest version you provide two inputs:
reaction(
() => data,
data => {
/* side effect */
},
);
The first function (the tracking function) is supposed to return the data that will be used to track. This data will then be passed into the second function (the effect function). The effect function is not tracked and you are free to use other observables here.
By default reaction
won’t run the first time and will wait for a change from the tracking function. Only when the data returned by the tracking function changes, will the side effect be executed. By breaking the original autorun into a tracking function
+ effect function
, you get more control around what is used for actually causing the side effect.
import { reaction } from 'mobx';
class Router {
@observable
page = 'main';
setupNavigation() {
reaction(
() => this.page,
page => {
switch (page) {
case 'main':
this.navigateToUrl('/');
break;
case 'profile':
this.navigateToUrl('/profile');
break;
case 'admin':
this.navigateToUrl('/admin');
break;
}
},
);
}
navigateToUrl(url) {
/* ... */
}
}
In the example above, I don’t want a navigation when I load the ’main’ page. A perfect case for using reaction
. Only when the page
property of the Router
changes, will the navigation happen to the specific url.
The above is a really simple Router with a fixed set of pages. You could make this more extensible by adding a map of pages to URLs. With such an approach, routing (with URL changes) becomes a side-effect of changing some property of your store.
when
to trigger one-time side-effectsautorun
and reaction
are long-running side-effects. You will create such side-effects when you initialize your application and expect them to run during the lifetime of your application.
One thing I didn’t mention before is that both these functions return a disposer function. You can call the disposer and cancel the side-effects any time.
const disposer = autorun(() => {
/* side-effects based on tracked observables */
});
// .... At a later time
disposer(); // Cancel the autorun
Now the apps we build have a variety of use cases. You may want certain side-effects to run only when you reach a certain point in your application. Also you may want these side-effects to only run once and then never again.
Let’s take a concrete example: Say, you want to show a message to the user when they reach a certain milestone in the app. This milestone will only happen once for any user so you don’t want to setup a long-running side-effect like autorun
or reaction
. It’s time you pull out the when
API to do this job.
when
takes two arguments, just like reaction
. The first one (the tracker-function) is supposed to return a boolean value. When this becomes true, it will run the effect-function, the second argument to when
. The best part is that it will auto-dispose the side-effect after it has run. So there is no need to keep track of the disposer and manually call it.
when(
() => this.reachedMilestone,
() => {
this.showMessage({ title: 'Congratulations', message: 'You did it!' });
},
);
Although I have only mentioned about action
, autorun
, reaction
and when
, there are few more APIs that MobX offers for some advanced use cases. I am choosing to deliberately ignore them for now as the bulk of MobX is really about using the above mentioned APIs. Once you become comfortable with these APIs, the rest is easier to follow and grasp. This is the foundation and we have to make this second nature before we start constructing advanced and fancy structures atop.
We will have a separate post about those advanced APIs. A Part-4 perhaps :-)
So far, we have seen a variety of techniques to track changes on an object graph and also react to those changes. MobX has raised the level of abstraction so that we can think at a higher-level without worrying about the accidental complexity of tracking and reacting to changes.
We now have a foundation on which we can build powerful systems that rely on changes to a domain-model. By treating everything outside the domain-model as a side-effect, we can provide visual feedback (UI) and perform a host of other activities like monitoring, analytics, logging, etc.
In Part 3, we will look at a variety of examples to apply all that we learnt so far in practice.
Huge thanks to Michel Westrate (author of MobX) for reviewing the content.