Migrating from Promise
Promise is the de-facto API to work with async data. Observables don't replace them, but they cover some specific limits of the Promises.
We will discuss of these particular use-cases in this tutorial.
Difference between a Promise and an Observable
First, we need to understand the differences between a Promise and an Observable:
- A Promise sends only one value, where an Observable may emit many values.
- A Promise cannot be cancelled (or with special tricks like
AbortSignal
). But, Observables are based on a subscribe/unsubscribe mechanism, so cancellation is native for them. - A Promise is executed immediately, whereas with Observables, we first define the pipeline, and then, by subscribing later, we run this pipeline.
So if we're facing one of these limits, then an Observable is the perfect candidate.
How to represent a Promise with an Observable ?
A Promise has an internal state:
pending
: the Promise is not resolved yetfulfilled
: the Promise is resolved without error and has avalue
rejected
: the Promise is rejected with anerror
However, an Observable may emit any kind of value. In order to mimic the behaviour of a Promise we will use some Notifications:
- INextNotification: used to emit a value
- ICompleteNotification: used to notify of a
complete
state (success/fulfilled) - IErrorNotification: used to notify of an
error
state (rejected)
To represent a fulfilled
Promise:
- we have to emit a
next
Notification - followed by a
complete
one
And to represent a rejected
Promise:
- we have to emit an
error
Notification
It's a little trick. We will see now some examples.
Observable equivalent of the Promise's methods
Promise.resolve
As said earlier, a fulfilled Promise, may be represented by a next
Notification followed by a complete
one.
This function has the name of singleWithNotifications.
It simply does:
function singleWithNotifications<GValue>(
value: GValue,
): IObservable<ISingleObservableNotifications<GValue>> {
return (emit: IObserver<ISingleObservableNotifications<GValue>>): IUnsubscribe => {
emit(createNextNotification<GValue>(value));
emit(STATIC_COMPLETE_NOTIFICATION);
return noop;
};
}
Promise.reject
On another hand, a rejected Promise, may be represented by an error
Notification.
This time, the function has the name of throwError.
And it does:
function throwError<GError>(
error: GError,
): IObservable<IErrorNotification<GError>> {
return (emit: IObserver<IErrorNotification<GError>>): IUnsubscribe => {
emit(createErrorNotification<GError>(error));
return noop;
};
}
Promise.all
Promise.all
is used to await all promises to fulfill from a list of promises. If one is rejected, it will reject too.
The Observable's equivalent is forkJoin or allWithNotifications.
Promise.race
Promise.race
is used to await the first promise to resolve (fulfilled or rejected) from a list of promises.
The Observable's function is: raceWithNotifications.
Promise.any
Promise.any
is used to await that one promise fulfills from a list of promises. If all the promises reject, it will reject too.
With Observables: anyWithNotifications.
Chaining -> then / catch
A key point of Promises is they capability to be chained using the .then
and .catch
methods.
then
The .then
method of a Promise accepts two optional/nullable arguments:
- an
onFulfilled
function (called when the Promise is fulfilled) - and an
onRejected
function (called when the Promise is rejected)
However, the Observables are stricter, and provides distinct functions, for each case:
.then(onFulfilled)
or.then(onFulfilled, null)
: fulfilledObservablePipe..catch(onRejected)
or.then(null, onRejected)
: rejectedObservablePipe..then(onFulfilled, onRejected)
: thenObservablePipe.
catch
So the .catch
method becomes rejectedObservablePipe, with Observables.
finally
Lastly, the .finally
method is used to handle fulfilled or rejected states.
With @lirx/core
, it's called finallyObservablePipe.
Casting an Observable to a Promise
In most cases, we simply want the last received value (last next
Notification), so we can use the function
toPromiseLast.
However, sometimes, we want to get all the values as an array. In this case, we can use the function
toPromiseAll.
.
toPromise MUST only be used with Observables NOT sending notifications.
Example
function noCORS(url: string): string {
const _url: URL = new URL(`https://cors-anywhere.herokuapp.com/`);
_url.pathname = url;
return _url.href;
}
interface IGeoJSGetGeoJSON {
organization_name: string;
region: string;
accuracy: number;
asn: number;
organization: string;
timezone: string;
longitude: string;
country_code3: string;
area_code: string;
ip: string;
city: string;
country: string;
continent_code: string;
country_code: string;
latitude: string;
}
// 1) prepare the request pipeline
const request$ = pipe$$(fromFetch(noCORS(`https://get.geojs.io/v1/ip/geo.json`)), [
fulfilled$$$((response: Response): IObservable<IDefaultNotificationsUnion<IGeoJSGetGeoJSON>> => {
if (response.ok) {
return fromPromiseFactory(() => response.json());
} else {
return throwError(createNetworkError());
}
}),
fulfilled$$$((data: IGeoJSGetGeoJSON): IObservable<IDefaultNotificationsUnion<string>> => {
return singleN<string>(data.country);
}),
]);
const doRequestOnClick$ = pipe$$(fromEventTarget(window, 'click'), [
switchMap$$$(() => request$),
]);
doRequestOnClick$((notification: IDefaultNotificationsUnion<string>) => {
console.log(notification.name, notification.value);
});
Output:
// user clicks
'next', United States
'complete', undefined