12

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)

Bart van den Burg
  • 2,166
  • 4
  • 25
  • 42
  • If you don't want to build a listener into each component, could you write a parent class that listens for changes from the service and have all your `ComponentB`s extend it? If not, you could use `input`s to those components, which will trigger an onPush change. – adamdport Apr 29 '19 at 14:01

1 Answers1

12

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.

Reactgular
  • 52,335
  • 19
  • 158
  • 208
  • I considered using a pipe for this (in fact, i'm using @ngx-translate which has `|translate` which appears to be doing what you suggest), but for some reason, even this I cannot get to work. See this little project here https://wetransfer.com/downloads/cc928ed32158761601b260b680cbd9e920190429142122/120f72. Cmp1 tries to render the value from the service using a pipe which calls markForCheck whenever the value changes, but it doesn't change in the view. What am I doing wrong here? – Bart van den Burg Apr 29 '19 at 14:25
  • I just realised my example doesn't work because I didn't set `pure` to `false` on the pipe, so after changing that it actually works! Now to figure out why it doesn't work in my project... – Bart van den Burg Apr 29 '19 at 14:49
  • For me your wetransfer link does not work. It's always the best idea to show the relevant code parts right here or to check in your code into GitHub for example. – Janos Vinceller Apr 02 '21 at 11:31