We can update the values of MobX observables. To do this, we can use the action
decorator or function.
In this article, we’ll look at how to use the action
decoration and decorator.
Usage
The action
decorator and function take a variety of form:
action(fn)
action(name, fn)
@action classMethod() {}
@action(name) classMethod () {}
@action boundClassMethod = (args) => { body }
@action(name) boundClassMethod = (args) => { body }
@action.bound classMethod() {}
Actions are anything that modifies the state. With MobX, we can make it explicitly in our code by marking them.
It takes a function that returns a function with the same signature, but it’s wrapped transaction
, untracked
, and allowStateChanges
.
Applying translation
increases the performance of actions, lets us batch mutation, and only notify computed values and reactions after the outermost actions has finished.
This makes sure that intermediate and incomplete values produced during action aren’t visible to the rest of the app until the action is done.
We can use action
on any function that modifies observables or has side effects.
action
also provides useful debugging information when we use the MobX dev tools.
Using action
decorator with setters isn’t supported.
action
is required when MobX is configured to require actions to make state changes with the enforceActions
option.
When to use actions?
Actions should be used on functions that modify state. Functions that just performs look-ups and filters shouldn’t be marked as actions so that their invocation can be tracked.
Bound Actions
action.bound
can be used to automatically bind actions to the targeted object. It doesn’t take a name parameter, so the name will always be based on the property name to which the action is bound.
Basic Example
We can use MobX actions as follows:
import { observable } from "mobx";
class Counter {
@observable count = 0;
@action.bound
increment() {
this.count++;
}
}
const counter = new Counter();
counter.increment();
In the code above, we have the Counter
class which has the count
observable field.
To update the count
, we have a bound action increment
to update the count
. Then we create a new Counter
instance and call the increment
method on it.
Writing Asynchronous Actions
Actions can also be used with a function that has asynchronous code like promises and setTimeout
.
For example, we can use it as follows:
import { observable, action } from "mobx";
import * as mobx from "mobx";
mobx.configure({ enforceActions: "observed" });
class Joker {
@observable joke = {};
@observable state = "pending";
@action
async fetchJoke() {
try {
const response = await fetch("https://api.icndb.com/jokes/random");
this.joke = response.json();
mobx.runInAction(() => {
this.state = "success";
});
} catch {
mobx.runInAction(() => {
this.state = "error";
});
}
}
}
const joker = new Joker();
joker.fetchJoke();
In the code above, we used the action
decorator to get a joke from the Chuck Norris API and then set it to our joke
observable field.
Then we created a new instance of Joker
and called the fetchJoke
method in it.
The runInAction
with its callback is needed to run actions after the await
is done. Therefore, we set the new value of this.state
in there.
We also have:
mobx.configure({ enforceActions: "observed" });
so that we can’t modify observable values outside actions.
flows
We can use flow
s as an alternative to async
and await
. With flow
s, we don’t have to call runInAction
to change observable values after await
is done.
It’s only available as a function.
For instance, we can rewrite the example as follows:
import { observable, action } from "mobx";
import * as mobx from "mobx";
mobx.configure({ enforceActions: "observed" });
class Joker {
@observable joke = {};
@observable state = "pending";
fetchJoke = mobx.flow(function*() {
try {
const response = yield fetch("https://api.icndb.com/jokes/random");
this.joke = response.json();
this.state = "success";
} catch {
this.state = "error";
}
});
}
const joker = new Joker();
joker.fetchJoke();
The difference between this and the previous example is that we used flow
, and that we passed in a generator function into it instead of an async
function.
We also remove the runInAction
calls since now we can set the observable values directly and the values will update correctly.
Promises with Then
If we use then
, then we have to use action.bound
on the callbacks we passed into then
.
For example, we can write the following:
import { observable, action } from "mobx";
import * as mobx from "mobx";
mobx.configure({ enforceActions: "observed" });
class Joker {
@observable joke = {};
@action.bound
fetchJokeSuccess(response) {
this.joke = response.json();
}
@action
fetchJoke() {
this.joke = {};
fetch("https://api.icndb.com/jokes/random").then(this.fetchJokeSuccess);
}
}
const joker = new Joker();
joker.fetchJoke();
The code above will ensure that the correct this
is applied to our fetchHJokeSuccess
callback.
To make the code shorter, we can rewrite it as follows:
import { observable, action } from "mobx";
import * as mobx from "mobx";
mobx.configure({ enforceActions: "observed" });
class Joker {
@observable joke = {};
@action
fetchJoke() {
this.joke = {};
fetch("https://api.icndb.com/jokes/random").then(
action("fetchJokeSuccess", response => {
this.joke = response.json();
})
);
}
}
const joker = new Joker();
joker.fetchJoke();
In the code above, we passed in the action
function with our callback directly to then
.
Conclusion
We can create actions with the action
decorator or function.
To create asynchronous actions, we can use async
functions with the runInAction
call.
We can also use the flow
function with a generator passed in.