I have a scenario where I have two components sitting alongside each other, ComponentA and ComponentB. They both use a service Service.
ComponentB contains a button which will make a property in Service, Service.counter, go up by 1.
ComponentA renders the value of Service.counter.
However, when I use ChangeDetectionStrategy.OnPush, no matter what I try, I cannot get the value in ComponentA to update, even from the root component, where I tried this:
this.cdr.markForCheck();
this.cdr.detectChanges();
this.ar.tick();
this.zone.run(() => {});
Without having to make changes to ComponentA, how can I make sure it always shows the right value?
(The real world scenario is that there's a lot of components like ComponentA all rendering translated values, and when the selected language changes, I need all these translated values to update accordingly. I don't want to build a listener into each individual component and call detectChanges from there)
One solution is to use the OnPush change detection strategy for specific components. This will instruct Angular to run change detection on these components and their sub-tree only when new references are passed to them versus when data is mutated. In this article, you will learn about ChangeDetectionStrategy and ChangeDetectorRef.
Angular provides us with three methods for triggering change detection ourselves when needed. The first is detectChanges() which tells Angular to run change detection on the component and his children. The second is ApplicationRef.tick() which tells Angular to run change detection for the whole application.
When using a change detection strategy of OnPush, other than making sure to pass new references every time something should change, you can also make use of the ChangeDetectorRef for complete control. You could for example keep mutating your data, and then have a button in the child component with a Refresh button.
The first is detectChanges() which tells Angular to run change detection on the component and his children. The second is ApplicationRef.tick() which tells Angular to run change detection for the whole application. The third is markForCheck() which does NOT trigger change detection.
However, when I use ChangeDetectionStrategy.OnPush, no matter what I try, I cannot get the value in ComponentA to update, even from the root component, where I tried this:
A component has an associated view. The view references the DOM and is what we want to be updated. When you use OnPush
the view of a component needs to be marked as dirty if the component's state changes externally.
When you say even from the root component
it means you're trying to mark the wrong view as dirty. If you want to see changes in ComponentA
then you need to flag that component view as dirty.
Something like this.
@Component({...})
public class ComponentA implements OnInit {
public count; // rendered in the view
public constructor(private _change: ChangeDetectorRef,
private _service: MyService) {
}
public onInit() {
this._service.getCounter().subscribe(value=> {
this.count = value; // will not update the view.
this._change.markForCheck(); // tell Angular it's dirty
});
}
}
So the above will work in 99% of the cases, but if the getCounter()
methods returns an observable that executes outside the scope of Angular, and you have to do this explicitly because async operations are automatically zoned, then you have to use the zone.run()
method. Otherwise, even if you mark the view dirty. Angular isn't going to check if any views need to be updated. This should not happen unless you're using non-Angular events or have explicitly run outside of Angular.
The alternative is to use the async
pipe, and is the easier approach.
@Component({
template: `<span>{{count$ | async}}</span>`
})
public class ComponentA implement OnInit {
public count$: Observable<number>;
public constructor(private _service: MyService) {
}
public onInit() {
this.count$ = this._service.getCounter();
}
}
The async
pipe uses a reference to ChangeDetectorRef
will also mark the view as dirty for you. So it saves you from a lot of boilerplate code.
The real world scenario is that there's a lot of components like ComponentA all rendering translated values, and when the selected language changes, I need all these translated values to update accordingly. I don't want to build a listener into each individual component and call detectChanges from there
Then you best bet is to use the async
pipe and make your components reactive.
If we're talking about something large scale and effects a lot of components, then maybe this root component should pass the value down to components as an @Input()
which will also trigger them to be rendered. While this creates a coupling between all of the components it says you from having to worry about updating the views.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With