Reputation: 3395
My app component is having a subscribe on a store select. I set ChangeDetectionStrategy
to OnPush
.
I have been reading about how this works; object reference needs to be updated to trigger a change.
When you use async pipe however, Angular expects new observable changes and do MarkForCheck for you.
So, why does my code not render the the channels (unless I call MarkForCheck
) when the subscribe is triggered and I set the channels$
a new observable array of channels.
@Component({
selector: 'podcast-search',
changeDetection: ChangeDetectionStrategy.OnPush, // turn this off if you want everything handled by NGRX. No watches. NgModel wont work
template: `
<h1>Podcasts Search</h1>
<div>
<input name="searchValue" type="text" [(ngModel)]="searchValue" ><button type="submit" (click)="doSearch()">Search</button>
</div>
<hr>
<div *ngIf="showChannels">
<h2>Found the following channels</h2>
<div *ngFor="let channel of channels$ | async" (click)="loadChannel( channel )">{{channel.trackName}}</div>
</div>
`,
})
export class PodcastSearchComponent implements OnInit {
channels$: Observable<Channel[]>;
searchValue: string;
showChannels = false;
test: Channel;
constructor(
@Inject( Store) private store: Store<fromStore.PodcastsState>,
@Inject( ChangeDetectorRef ) private ref: ChangeDetectorRef,
) {}
ngOnInit() {
this.store.select( fromStore.getAllChannels ).subscribe( channels =>{
if ( channels.length ) {
console.log('channels', !!channels.length, channels);
this.channels$ = of ( channels );
this.showChannels = !!channels.length;
this.ref.markForCheck();
}
} );
}
I tried multiple solutions, including using a subject
and calling next
, but that doesn't work unless I call MarkForCheck.
Can anyone tell me how I can avoid calling markForCheck
?
Upvotes: 6
Views: 5055
Reputation: 191729
This may be a bit difficult to explain, but I'll give it my best attempt. When your original Observable (the store) emits it is not bound to the template. Since you're using OnPush change detection, when this observable emits it doesn't mark the component for changes because of the lack of binding.
You are attempting to trigger a mark for changes by overwriting a component property. Even though you are creating a new reference on the component property itself, this doesn't mark the component for changes because the component is changing its own property rather than a new value being pushed onto the component.
You are correct in thinking that the async pipe marks the component for changes when a new value emits. You can see this in the Angular source here: https://github.com/angular/angular/blob/6.0.9/packages/common/src/pipes/async_pipe.ts#L139
However you will note that this only works if the value (called async
), the property that you are using with the async
pipe, matches this._obj
, the Object that the async
pipe has already recorded as being the Observable that is emitting.
Since you are doing channels$ = <new Observable>
, async === this._obj
is actually untrue since you are changing the object references. This is why your component is not marked for changes.
You can also see this in action in a Stackblitz I've put together. The first component overwrites the Observable passed to async
pipe whereas the second does not overwrite it and updates the data by responding to changes that are emitted -- this is what you want to do:
https://stackblitz.com/edit/angular-pgk4pw (I use timer
because it's an easy way to simulate a third-party unbound Observable source. Using an output binding, e.g. updating on click, is more difficult to set up since if it's done in the same component the output action will trigger a mark for changes).
All is not lost for you -- I would suggest that you do this.channels$ = this.store.select(...)
instead. The async
pipe handles .subscribe
for you. If you're using the async
pipe you shouldn't be using .subscribe
anyway.
this.channels$ = this.store.select(fromStore.getAllChannels).pipe(
filter(channels => channels.length),
// channels.length should always be truthy at this point
tap(channels => console.log('channels', !!channels.length, channels),
);
Note that you can use ngIf
with the async pipe as well which should obviate your need for showChannels
.
Upvotes: 5