Reputation: 850
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.
Upvotes: 0
Views: 141
Reputation: 9618
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!
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!
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 🙂) overlooked something earlier:
Angular isn't simply re-evaluating getOutputs()
until your component is stable, but actually getOutputs() | async
, which translates roughly as async(getOutput())
in a pure TypeScript world.
What is happening here is also somewhat simple:
Contrary to the previous case, where the AsyncPipe
, when having to wait for a delay
ed Observble
, won't be definitely stable; that AsyncPipe
yields a value immediately and yields the same ('done'
) every time; Angular considers your component "stable" and proceeds with updating the view.
If you debug the code attentively (or drop a console.log
at the top of getOutputs()
's body), you can notice that getOutputs()
is then still called 4 times in rapid succession, one for each evaluation of async(getOutputs())
.
Switching to a more conservative changeDetection
strategy for your component as follows:
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush, // different strategy here
})
export class AppComponent implements OnInit, OnDestroy {
... would allow getOutputs()
to only be executed once in that case!
Upvotes: 1