'Can promises in Angular pipes break an application?
I have this small library that provides a translation pipe. Everything works well, if I use Observables. The relevent code is here:
this.localizationChanges.subscribe(() => {
this.fluentService
.translationObservable(key, args, getLocale(args))
.subscribe(value => this.value = value);
});
I would like to switch to Promises, so the pipe only switches values, once the locale is loaded and a translation is found. But if I change the code to the following the application can't even load anymore (it compiles fine).
this.localizationChanges.subscribe(() => {
this.fluentService
.translate(key, args, getLocale(args))
.then(value => this.value = value);
});
Is this something that is not allowed in Angular? Why does it compile and then end up showing an empty page without even printing an error message?
Solution 1:[1]
First, you can do this with RXJS, or promises, but either way intentionally makes room for asynchronous programming, so when your transform method synchronously returns this.value at the end, I don't think you're getting what you're expecting? I'm guessing the reason it is compiling but you don't think it is working is that it is working, but the correct value is not computed before you're using it.
To stay in observables, it should return an observable.
transform(key: string, args?: any): Observable<string | null> {
if (!this.localizationChanges) {
this.localizationChanges = this.fluentService.localizationChanges;
}
return this.localizationChanges.pipe(
switchMap(() => this.fluentService
.translationObservable(key, args, getLocale(args))
)
);
}
and then in your template, chain | async to the end of it. The async pipe will take care of subscribing, unsubscribing, telling the component to refresh every time the source observable emits or is changed, etc.
switchMap causes any still-waiting results of fluentService.translationObservable to be dropped each time localizationChanges emits, replacing with a new call.
If you only want one value emitted, then promises are an alternative. In that case, you'd probably want
async transform(key: string, args?: any): Promise<string | null> {
if (!this.localizationChanges) {
this.localizationChanges = this.fluentService.localizationChanges;
}
return this.localizationChanges.toPromise().then(
() => this.fluentService.translate(
key, args, getLocale(args)
)
);
}
and then in your template, chain | async to the end of it rather than recreate that piece of code.
Solution 2:[2]
If I understand the problem well, your code has a fundamental problem and the fact that "Everything works well, if I use Observables" is a fruit of a very special case.
Let's look at this very stripped down version of your code
function translationObservable(key) {
return of("Obs translates key: " + key);
}
function transform(key: string, args?: any): string | null {
let retValue: string;
const localizationChanges: BehaviorSubject<null> = new BehaviorSubject<null>(
null
);
localizationChanges.subscribe(() => {
translationObservable(key).subscribe((value) => (retValue = value));
});
return retValue;
}
console.log(transform("abc")); // prints "Obs translates key: abc"
If you run this code you actually end up with a message printed out on the console.
There is one reason why this code works and the reason is that we are using Observables synchronously. In other words the code is executed line after line and therefore the assignment retValue = value ocurs before retValue is returned.
Promises though are intrinsically asynchronous. So, whatever logic you pass to the then method gets executed asynchronously, i.e. in another subsequent cycle of the Javascript engine.
This means that if we use a Promise instead of an Observable in the same example as above, we will not get any message printed
function translationPromise(key) {
return Promise.resolve("Promise translates key: " + key);
}
let retValue: string;
function transform(key: string, args?: any): string | null {
const localizationChanges: BehaviorSubject<null> = new BehaviorSubject<null>(
null
);
localizationChanges.subscribe(() => {
translationPromise(key).subscribe((value) => (retValue = value));
});
return retValue;
}
console.log(transform("abc")); // prints undefined
// some time later it prints the value returned using Promises
setTimeout(() => {
console.log(retValue);
}, 100);
The summary is that your code works probably only when you use the of operator to create the Observable (which works synchronously since there is no async operation involved) but I doubt it works when it has to invoke async functionalities like fetching a locale file.
If you want to build an Angular pipe which works asynchronously you need to follow the answer of @JSmart523
As last sode note, in your code you are subscribing an Observable within a Subscrition. This is considered not idiomatic with Observables.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Roy |
| Solution 2 | Picci |
