Define a computed by returning a Promise
"People starting with MobX tend to use reactions [autorun] too often. The golden rule is: if you want to create a value based on the current state, use computed." - MobX - Concepts & Principles
A computed in MobX is defined by a function, which consumes other observable values and is automatically re-evaluated, like a spreadsheet cell containing a calculation.
@computed get creditScore() {
return this.scoresByUser[this.userName];
}
However, it has to be a synchronous function body. What if you want to do something asynchronous? e.g. get something from the server. That's where this little extension comes in:
creditScore = promisedComputed(0, async () => {
const response = await fetch(`users/${this.userName}/score`);
const data = await response.json();
return data.score;
});
Further explanation, rationale, etc.
This is the most capable function. It is actually just a composition of two simpler functions,
promisedComputed and throttledComputed, described below.
init - the value to assume until the first genuine value is returneddelay - the minimum time in milliseconds to wait between creating new promisescompute - the function to evaluate to get a promise (or plain value)A Mobx-style getter, i.e. an object with a get function that returns the current value. It
is an observable, so it can be used from other MobX contexts. It cannot be used outside
MobX reactive contexts (it throws an exception if you attempt it).
The returned object also has a busy property that is true while a promise is still pending.
It also has a refresh method that can be called to force a new promise to be requested
immediately (bypassing the delay time).
There is also a method getNonReactive() which can be used outside reactive contexts. It is
a convenience for writing unit tests. Note that it will return the most recent value that was
computed while the asyncComputed was being observed.
fullName = asyncComputed("(Please wait...)", 500, async () => {
const response = await fetch(`users/${this.userName}/info`);
const data = await response.json();
return data.fullName;
});
The value of fullName.get() is observable. It will initially return
"(Please wait...)" and will later transition to the user's full name.
If the this.userName property is an observable and is modified, the
promisedComputed will update also, but after waiting at least 500
milliseconds.
Like asyncComputed but without the delay support. This has the slight advantage
of being fully synchronous if the compute function returns a plain value.
init - the value to assume until the first genuine value is returnedcompute - the function to evaluate to get a promise (or plain value)Exactly as asyncComputed.
fullName = promisedComputed("(Please wait...)", async () => {
const response = await fetch(`users/${this.userName}/info`);
const data = await response.json();
return data.fullName;
});
The value of fullName.get() is observable. It will initially return
"(Please wait...)" and will later transition to the user's full name.
If the this.userName property is an observable and is modified, the
promisedComputed will update also, as soon as possible.
Like the standard computed but with support for delaying for a specified number of
milliseconds before re-evaluation.
(Note that throttledComputed has no special functionality for handling promises.)
compute - the function to evaluate to get a plain valuedelay - the minimum time in milliseconds to wait before re-evaluatingA Mobx-style getter, i.e. an object with a get function that returns the current value. It
is an observable, so it can be used from other MobX contexts. It can also be used outside
MobX reactive contexts but (like standard computed) it reverts to simply re-evaluating
every time you request the value.
It also has a refresh method that immediately (synchronously) re-evaluates the function.
The value returned from get is always a value obtained from the provided compute function,
never silently substituted.
fullName = throttledComputed(500, () => {
const data = slowSearchInMemory(this.userName);
return data.fullName;
});
The value of fullName.get() is observable. It will initially return the result of the
search, which happens synchronously the first time. If the this.userName property is an
observable and is modified, the throttledComputed will update also, but after waiting at
least 500 milliseconds.
Much like the standard autorun with the delay option, except that the initial run of
the function happens synchronously.
(This is used by throttledComputed to allow it to be synchronously initialized.)
func - The function to execute in reactiondelay - The minimum delay between executionsname - (optional) For MobX debug purposesA Mobx-style getter, i.e. an object with a get function that returns the current value. It
is an observable, so it can be used from other MobX contexts. It can also be used outside
MobX reactive contexts but (like standard computed) it reverts to simply re-evaluating
every time you request the value.
npm install computed-async-mobx
Of course TypeScript is optional; like a lot of libraries these days, this is a JavaScript
library that happens to be written in TypeScript. It also has built-in type definitions: no
need to npm install @types/... anything.
I first saw this idea on the Knockout.js wiki in 2011. As discussed here it was tricky to make it well-behaved re: memory leaks for a few years.
MobX uses the same (i.e. correct) approach as ko.pureComputed from the ground up, and the Atom class makes it easy to detect when your data transitions
between being observed and not. More recently I realised fromPromise in mobx-utils
could be used to implement promisedComputed pretty directly. If you don't need throttling (delay parameter) then all
you need is a super-thin layer over existing libraries, which is what promisedComputed is.
Also a :rose: for Basarat for pointing out the need to support strict mode!
Thanks to Daniel Nakov for fixes to support for MobX 4.x.
Unlike the normal computed feature, promisedComputed can't work as a decorator on a property getter. This is because it changes the type of the return value from PromiseLike<T> to T.
Instead, as in the example above, declare an ordinary property. If you're using TypeScript (or an ES6 transpiler with equivalent support for classes) then you can declare and initialise the property in a class in one statement:
class Person {
@observable userName: string;
creditScore = promisedComputed(0, async () => {
const response = await fetch(`users/${this.userName}/score`);
const data = await response.json();
return data.score; // score between 0 and 1000
});
@computed
get percentage() {
return Math.round(this.creditScore.get() / 10);
}
// For MobX 6
constructor() {
makeObservable(this);
}
}
Note how we can consume the value via the .get() function inside another (ordinary) computed and it too will re-evaluate when the score updates.
This library is transparent with respect to MobX's strict mode, and since 4.2.0 this is true even of the very strict "always" mode that doesn't even let you initialize fields of a class outside a reactive context.
Take care when using async/await. MobX dependency tracking can only detect you reading data in the first "chunk" of a function containing awaits. It's okay to read data in the expression passed to await (as in the above example) because that is evaluated before being passed to the first await. But after execution "returns" from the first await the context is different and MobX doesn't track further reads.
For example, here we fetch two pieces of data to combine them together:
answer = asyncComputed(0, 1000, async () => {
// Don't do this!!
const part1 = await fetch(this.part1Uri),
part2 = await fetch(this.part2Uri);
// combine part1 and part2 into a result somehow...
return part1 + part2;
});
The properties part1Uri and part2Uri are ordinary mobx observables (or computeds). You'd expect that when either of those values changes, this asyncComputed will re-execute. But in fact it can only detect when part1Uri changes. When an async function is called, only the first part (up to the first await) executes immediately, and so that's the only part that MobX will be able to track. The remaining parts execute later on, when MobX has stopped listening.
(Note: the expression on the right of await has to be executed before the await pauses the function, so the access to this.part1Uri is properly detected by MobX).
We can work around this like so:
answer = asyncComputed(0, 1000, async () => {
const uri1 = this.part1Uri,
uri2 = this.part2Uri;
const part1 = await fetch(uri1),
part2 = await fetch(uri2);
// combine part1 and part2 into a result somehow...
return result;
});
When in doubt, move all your gathering of observable values to the start of the async function.
Versions prior to 3.0.0 had a different API. It was a single computedAsync function that had all the
capabilities, like a Swiss-Army Knife, making it difficult to test, maintain and use. It also had some
built-in functionality that could just as easily be provided by user code, which is pointless and only
creates obscurity.
computedAsync with a zero delay, use promisedComputed, which takes no delay
parameter.computedAsync with a non-zero delay, use asyncComputed.value property, call the get() function (this is for closer consistency with
standard MobX computed.)revert, use the busy property to decide when to substitute a different value.rethrow property made computedAsync propagate exceptions. There is no need to request this
behaviour with promisedComputed and asyncComputed as they always propagate exceptions.error property computed a substitute value in case of an error. Instead, just do this substitution
in your compute function.See CHANGES.md.
MIT, see LICENSE
Generated using TypeDoc