Writing Asynchronous Actions
The action
wrapper / decorator only affects the currently running function, not functions that are scheduled (but not invoked) by the current function! This means that if you have a setTimeout
, promise .then
or async
construction, and in that callback some more state is changed, those callbacks should be wrapped in action
as well! There are several ways to create asynchronous actions. No approach is strictly better than the other, but this section to just lists different approaches you can take to writing asynchronous code.
Promises
Extract the callbacks to actions. (Note: Binding is important here to get a correct this
!)
class Store {
@observable githubProjects = []
@observable state = "pending" // "pending" / "done" / "error"
@action
fetchProjects() {
this.githubProjects = []
this.state = "pending"
fetchGithubProjectsSomehow().then(this.fetchProjectsSuccess, this.fetchProjectsError)
}
@action.bound // callback action
fetchProjectsSuccess(projects) {
const filteredProjects = somePreprocessing(projects)
this.githubProjects = filteredProjects
this.state = "done"
}
@action.bound // callback action
fetchProjectsError(error) {
this.state = "error"
}
}
Although this is clean and explicit, it might get a bit verbose with complex async flows. Alternatively, you can wrap the promise callbacks with the action
keyword. It is recommended, but not mandatory, to give them a name as well:
mobx.useStrict(true) // don't allow state modifications outside actions
class Store {
@observable githubProjects = []
@observable state = "pending" // "pending" / "done" / "error"
@action
fetchProjects() {
this.githubProjects = []
this.state = "pending"
fetchGithubProjectsSomehow().then(
// inline created action
action("fetchSuccess", projects => {
const filteredProjects = somePreprocessing(projects)
this.githubProjects = filteredProjects
this.state = "done"
}),
// inline created action
action("fetchError", error => {
this.state = "error"
})
)
}
}
runInAction
A downside of inline actions is that TypeScript does not apply type inference on them, so you would have to type all your callbacks. Instead of creating an action of the entire callback, you can also run only the state modifying part of the callback in an action. The advantage of this pattern is that it encourages you to not litter the place with action
, but rather put all the state modifications as much as possible at the end of the whole process:
mobx.useStrict(true) // don't allow state modifications outside actions
class Store {
@observable githubProjects = []
@observable state = "pending" // "pending" / "done" / "error"
@action
fetchProjects() {
this.githubProjects = []
this.state = "pending"
fetchGithubProjectsSomehow().then(
projects => {
const filteredProjects = somePreprocessing(projects)
// put the 'final' modification in an anonymous action
runInAction(() => {
this.githubProjects = filteredProjects
this.state = "done"
})
},
error => {
// the alternative ending of this process:...
runInAction(() => {
this.state = "error"
})
}
)
}
}
Note:
runinAction
's can also be given a name as first argument.runInAction(f)
is in fact just sugar foraction(f)()
.
async
/ await
Async / await-based functions can initially seem confusing when starting with actions. Because lexically they appear to synchronous functions, it gives the impression that @action
applies to the entire function. Which is, of course, not the case, as async / await is just syntactic sugar around a promise-based process. As a result, @action
only applies to the code block until the first await
. And after each await
, a new synchronous function is started, so after each await
, state modifying code should be wrapped as action. This is where runInAction
comes in handy again:
mobx.useStrict(true) // don't allow state modifications outside actions
class Store {
@observable githubProjects = []
@observable state = "pending" // "pending" / "done" / "error"
@action
async fetchProjects() {
this.githubProjects = []
this.state = "pending"
try {
const projects = await fetchGithubProjectsSomehow()
const filteredProjects = somePreprocessing(projects)
// after await, modifying state again, needs an actions:
runInAction(() => {
this.state = "done"
this.githubProjects = filteredProjects
})
} catch (error) {
runInAction(() => {
this.state = "error"
})
}
}
}
babel-plugin-mobx-deep-action
If you use Babel, there is a plugin that scans for @action
methods and automatically wraps all callbacks and await statements properly in actions during transpilation: mobx-deep-action
.
Generators & asyncAction
Finally, there is the asyncAction
utility in the mobx-utils
package that uses generators behind the scenes to automatically wrap yielded promises in actions. The advantage of this is that it is syntactically very close to async / await (with different keywords), so no manual action wrapping is needed for async parts, resulting in very clean code. Just make sure every yield
returns a promise.
asyncAction
can be used as decorator and as function (just like @action
). asyncAction
integrates neatly with MobX development tools, so that it is easy to trace the process of the async function.
import {asyncAction} from "mobx-utils"
mobx.useStrict(true) // don't allow state modifications outside actions
class Store {
@observable githubProjects = []
@observable state = "pending" // "pending" / "done" / "error"
@asyncAction
*fetchProjects() { // <- note the star, this a generator function!
this.githubProjects = []
this.state = "pending"
try {
const projects = yield fetchGithubProjectsSomehow() // yield instead of await
const filteredProjects = somePreprocessing(projects)
// the asynchronous blocks will automatically be wrapped actions
this.state = "done"
this.githubProjects = filteredProjects
} catch (error) {
this.state = "error"
}
}
}