I have a problem with the async
-pipe in combination with an Observable
which gets it's first value in OnInit
. Must be a timing issue about the point in time when OnInit
happens and the one when the template gets rendered and thus the Observable
get subscribed.
Consider this component:
export class AppComponent implements OnInit {
subjectA$: Subject<{name:string}>;
subjectB$: Subject<{name:string}>;
constructor(
protected http: HttpClient
) {
}
ngOnInit() {
this.subjectA$ = new Subject<{name: string}>();
this.subjectA$.next({name: "A"});
this.subjectB$ = new Subject<{name: string}>();
setTimeout(() => {
this.subjectB$.next({name: "B"});
}, 0);
}
}
and the template:
<p *ngIf='subjectA$ | async as subjectA; else: nosubjectA'>
subjectA: {{subjectA.name}}
</p>
<ng-template #nosubjectA>
<p>no subjectA</p>
</ng-template>
<p *ngIf='subjectB$ | async as subjectB; else: nosubjectB'>
subjectB: {{subjectB.name}}
</p>
<ng-template #nosubjectB>
<p>no subjectB</p>
</ng-template>
This results in
no subjectA
subjectB: B
That means: Even if subjectA$
got a value in onInit
, the view is not updated. If I wrap around the creation of the first value in a setTimeout
as you can see with subjectB$
, it works and I see the value. Although this is a solution I am wondering why does this this happen and is there a better solution?
One solution I already found would be using BehaviorSubject
instead an provide the first value as initial value:
this.subjectC$ = new BehaviorSubject<{name: string}>({name: "C"});
leads to subjectC: C
with analogous template for subjectC.
My real observable is no Subject
at all but the result of a combineLatest
-call of different stuff, from which only one is (and have to unfortunately since it is using a value from an @Input()
-annotation) a Subject
, and manually pushed with next
in OnInit
as in the example. The rest comes from http
et al. Most likely I could wrap the combined result in a BehaviourSubject
but it seems ugly and dangerous to me, so it's even worse then the setTimeout
approach. But I bet someone can help me out and find a real useful solution.
In addition, I would prefer to avoid BehaviorSubject
, to prevent developers from being tempted to use getValue
.
After the comment I made, I really couldn't help but think that there must be a better way - and finally thought of something that worked!
I just modified your stackblitz a bit.
private valueA = "A";
private valueB = "B";
subjectA$ = of({ name: this.valueA });
subjectB$ = of({ name: this.valueB });
subjectC$ = combineLatest([this.subjectA$, this.subjectB$])
.pipe(
map((things: [{name:string}, {name:string}]): {name:string} => {return {name: things.map(x => x.name).join('|')}})
);
This way, we can even discard the ngOnInit
hook, and everything works at it should!