4

I don't quite understand why i need to add markForCheck() to the code below to make the changes visible. Why is my @Input() not triggering change detection?

I am refactoring my project to OnPush. These 2 components both have OnPush enabled. As i understand it, when this is enabled, and an Input() is changed (like messages) the change detection gets triggered.

In the code below i make a call via the graphqlService. When i receive the call i do some parsing with the incoming data and then set it to informationMessages property, which is binded to the child component cv-messages through its messages property.

The result is that the ngOnChanges function only get called once, when the informationMessages property gets initialized. But not when the final parsed data is set to it.

If I add markForCheck() it works fine.

Consider this parent component, with a template like this:

<cv-messages [messages]="informationMessages"></cv-messages>

And a typescript file with this piece of code:

informationMessages: InformationMessageType[] = [];

ngOnInit() {
    this.graphqlService.loadInformationMessages().subscribe(data => {
        const informationMessages: InformationMessageType[] = data;

        .... // parsing stuff

        this.informationMessages = informationMessages;
        // add markForCheck() here
    });
}

The messages component has an ngOnChanges function like this:

ngOnChanges(changes) {
    console.log(this.constructor.name ' CHANGE:', changes);
}

Update:

You can find the solution in the comments of an answer below. Basically, change detection is NOT triggered when an @Input() changes asynchronously. So in that case we need to add a markForCheck() to force a change detection.

Martijn van den Bergh
  • 1,434
  • 1
  • 20
  • 40

1 Answers1

5

As Angular docs says:

When a view uses the OnPush (checkOnce) change detection strategy, explicitly marks the view as changed so that it can be checked again.

Components are normally marked as dirty (in need of rerendering) when inputs have changed or events have fired in the view. Call this method to ensure that a component is checked even if these triggers have not occured.

So this method is needed to to mark a component as dirty to be rerendered.

UPDATE:

There are two types of ChangeDetectionStrategy:

OnPush: 0 Use the CheckOnce strategy, meaning that automatic change detection is deactivated until reactivated by setting the strategy to Default (CheckAlways). Change detection can still be explicitly invoked. This strategy applies to all child directives and cannot be overridden.

Default: 1 Use the default CheckAlways strategy, in which change detection is automatic until explicitly deactivated.

So when you use OnPush then automatic change detection is deactivated and it is necessary to mark the view as changed so that it can be checked again.

StepUp
  • 36,391
  • 15
  • 88
  • 148
  • So what you are saying is that Angular missed this property being dirty so i need to explicitly set it myself? But why did Angular miss this dirty change? Am i doing something wrong? – Martijn van den Bergh Dec 09 '19 at 09:17
  • 1
    @MartijnvandenBergh you are doing correctly. Please, see my updated answer – StepUp Dec 09 '19 at 09:25
  • Thanks, all very clear. Still find it weird that i have to do it manually because i am changing an `@Input()` (informationMessage is binded to [messages]) which i thought would force a change detection with OnPush components? But i guess my understanding was wrong. – Martijn van den Bergh Dec 09 '19 at 09:28
  • @MartijnvandenBergh It might be because of asynchronous actions that you execute on `ngOnInit()` – Alex Beugnet Dec 09 '19 at 09:50
  • @MartijnvandenBergh there is a great answer about [why onPush does not update child property from parent](https://stackoverflow.com/questions/49865267/angular-onpush-does-not-update-child-property-from-parent). – StepUp Dec 09 '19 at 09:55
  • @AlexBeugnet Could you elaborate? – Martijn van den Bergh Dec 09 '19 at 09:58
  • 1
    @MartijnvandenBergh In your `ngOnInit()`, you execute actions that aim to set the `informationMessages` property. The issue is that Angular doesn't refresh the view since you set this property "asynchroniously" and thus the view doesn't get updated as no `@Input` has been changed. Usually, you set OnPush with components that directly use the `@Input` properties in the html so that whenever a new value is pushed to the component, Angular knows when to launch a detection cycle. In this case you manually set a property, and Angular doesn't know what to do.This is why you launch detection manually – Alex Beugnet Dec 09 '19 at 13:08
  • Okay thanks, i thought that regardless of async a change detection would be triggered if an @Input() was changed. Thanks for clarifying. – Martijn van den Bergh Dec 09 '19 at 13:11