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 avaluerejected: 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
completestate (success/fulfilled) - IErrorNotification: used to notify of an
errorstate (rejected)
To represent a fulfilled Promise:
- we have to emit a
nextNotification - followed by a
completeone
And to represent a rejected Promise:
- we have to emit an
errorNotification
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
onFulfilledfunction (called when the Promise is fulfilled) - and an
onRejectedfunction (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