Issue
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.
Solution
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 🙂) 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!
Answered By - ccjmne
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.