Skip to content

Why do we need this?

Daniel Earwicker edited this page Dec 24, 2016 · 1 revision

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.

Clone this wiki locally