-
Notifications
You must be signed in to change notification settings - Fork 6
Why do we need this?
Suppose we had an unfinished model like this:
class Person {
@observable userName: string;
@observable creditScore: number;
@computed
get percentage() {
return Math.round(this.creditScore / 10);
}
}
When the userName
changes we want to ask the server for that user's credit score and so update the creditScore
observable. So we try writing something like:
class Person {
@observable userName: string;
@observable creditScore: number;
constructor() {
autorunAsync(async () => {
const response = await fetch(`users/${this.userName}/score`);
const data = await response.json();
this.creditScore = data.score;
}, 500); // minimum delay before updating (milliseconds)
}
@computed
get percentage() {
return Math.round(this.creditScore / 10);
}
}
There are two problems. First, an ordering bug that can leave stale data in place. If the user changes the userName
twice, the first request might take longer to finish than the second. This means that the second response will get overwritten by the first. This leaves the UI in an inconsistent state. computedAsync
is designed to stop this happening: only the most recently launched request is allowed to publish its response.
The second problem is more subtle: the return value of autorunAsync
is being ignored. You're supposed to call it when you want the updates to stop (it's a "disposer function").
In simple examples like this it's not a problem. When you discard an instance of Person
it's fine if it has internals that depend on other internals of the same object.
Where it goes wrong is if the body of the autorunAsync
is ever modified so it reads the value of something with a longer lifetime than Person
. This can be very hard to notice yourself doing! But it will cause the autorunAsync
to keep executing (making unnecessary calls to the server) whenever that external value changes. Basically every observable in MobX has a list of objects that depend on it, and merely by reading the current value you can get your object added to that list, which keeps it alive.
MobX has the right pattern for solving this automatically for computed
values: they only actively listen to their dependencies when they too are being listened to. So they can listen to whatever they want without being kept alive too long. computedAsync
follows this same pattern. Internally it has an autorunAsync
but it creates/disposes for you, ensuring that it is only active when you are actually using your computedAsync
value.
So computedAsync
takes care of these problems, and does so in a neat package that is closely analogous to the existing computed
feature of MobX. The difference is the evaluator function returns a Promise<T>
instead of a Promise
.