If I have an Observable
releasing values over time:
values$ = from([1, 2, 3, 'done'])
.pipe(
concatMap((x) => of(x).pipe(delay(1000)))
);
And I have a function returning access to that Observable
:
getOutputs(): Observable<'done' | number> {
return this.values$;
}
And subscribe to the Observable
through a function in the template using *ngIf
and async
:
<div *ngIf="getOutputs() | async as val">
<hello name="{{ val }}"></hello>
</div>
The behavior is expected: the browser shows 'Hello 1!', 'Hello 2!', 'Hello 3!', 'Hello done!', with an interval for each of about a second.
If, instead, I store the latest value in a BehaviorSubject
and cycle all of the values through that BehaviorSubject
in ngOnInit
:
outputs$ = new BehaviorSubject<number | 'done' | null>(null);
ngOnInit(): void {
this.subscriptions.add(
from<[number, number, number, 'done']>([
1,
2,
3,
'done',
]).subscribe((val) => this.outputs$.next(val))
);
}
The behavior is of course different: the values are all sent to the BehaviorSubject
, and outputs$.value
becomes 'done' very quickly. So anything coming along later and subscribing would only get 'done'. Also expected.
If I change getOutputs()
to use this.outputs$
instead, I just get 'Hello done!':
getOutputs(): Observable<null | 'done' | number> {
return this.outputs$;
}
But if I add the same concatMap
used earlier, like this:
getOutputs(): Observable<null | 'done' | number> {
return this.outputs$
.pipe(
concatMap((x) => of(x).pipe(delay(1000)))
);
}
'done' gets sent over and over, once a second (which can be seen through tap(console.log)
), but the template shows nothing. This is unexpected: I would think that the HTML would show 'Hello done!'.
Why is this happening?
See this Stackblitz.
CodePudding user response:
TL;DR
This is caused by how Angular handles change detection.
Angular will regularly check that your view is up-to-date with the data in your model, and is in fact continuously calling your getOuputs()
method, once every second!
Angular's Change Detection in a nutshell
Consider your app.component.html
template:
<div *ngIf="getOutputs() | async as val">
<hello name="{{ val }}"></hello>
</div>
Here, Angular will regularly re-evaluate getOutputs() | async
, a "couple of times", until your application is in a stable state.
However, for each evaluation, you are returning a new, unique Observable
, because you create that new, unique Observable
in your getOutputs
method:
public getOutputs(): Observable</* ... */> {
return this.outputs$.pipe(
concatMap(x => of(x).pipe(delay(1000))),
); // it's not `this.outputs$`, it's a **new Observable**
}
Therefore, if you where to create another member like that:
export class AppComponent implements OnInit, OnDestroy {
private subscriptions = new Subscription();
private outputs$ = new BehaviorSubject</* ... */>(null);
private actualOutputs$ = this.outputs$.pipe(
concatMap(x => of(x).pipe(delay(1000))),
);
public getOutputs(): Observable</* ... */> {
return this.actualOutputs$;
- return this.outputs$.pipe(
- concatMap(x => of(x).pipe(delay(1000))),
- );
}
}
(welp, StackOverflow doesn't support diff
syntax highlighting, sorry about that...)
... then your application would behave exactly as you expect!
Some more exploration
But then why does removing the delay, yet yielding a different Observable
every time also works?
Let's consider the alternative implementation for getOutputs
below:
public getOutputs(): Observable</* ... */> {
return this.outputs$.pipe(
concatMap(x => of(x)/* .pipe(delay(1000)) */), // no more delay here
); // that's a **new** Observable every time.
}
It's because I (purposely