Angular Change detection not working with ChangeDetectionStrategy.OnPush in HttpClinet.Subscribe
Asked Answered
S

4

7

I have reproduced a simple stackblitz demonstrating the issue I have been having. The problem is that I have a parent component that passes a boolean to a child component. This boolean is an @Input on the child component. It is important to note that the parent component uses ChangeDetectionStrategy.OnPush. The child component does not have this explicitly set.

When the parent component changes the boolean input property of the child component within a subscribe method, the child component does not initially detect the change. It always take 2 clicks to have the child component detect the change.

However, when I change the boolean input propery of the child component outside of a subscribe method, the child component that properly detects the change, and everything works as expected (1 click for child component to recognize change).

App.Component.ts (Parent Component)

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent  {
  constructor(private http: HttpClient) {

  }
  public isHelloVisible: boolean;
  public useHttpGet: boolean;

  showHello() {
    if (this.useHttpGet) {
    this.http.get('https://cors-anywhere.herokuapp.com/https://api.darksky.net/forecast/cc0e3799790b0b34bdeb6fef28c3daf7/17.447409200000003,-78.3724573?units=si').subscribe(data => {
      this.isHelloVisible = true;  
    });
    } else {
      this.isHelloVisible = true;
    }
  }

  closeHello() {
    this.isHelloVisible = false;    
  }

Child Component (Hello.component.ts)

@Component({
  selector: 'hello',
  template: `<div *ngIf="showHello">
    Hello
    <div (click)="closeHello()">Click me to close Hello</div>
  </div>
  
  `,
  styles: []
})
export class HelloComponent  {
  @Input() showHello: boolean;
  @Output() close: EventEmitter<any> = new EventEmitter();

  ngOnChanges(changes: SimpleChanges): void {
    console.log(this.showHello);
  }

  closeHello() {
    this.close.emit(null);
  }

if useHttpGet is true it does not work and if false everything works.

I realize there may be different ways to do this or manually trigger change detection, but I am more interested in why this does not work, as this does not make any sense to me.

Probably the best way to see this in action is to follow the stackblitz demo.

Salleysalli answered 10/7, 2020 at 22:23 Comment(0)
J
4

The parent is set to OnPush which means it only runs change detection in certain circumstances:

  1. The Input reference changes. You don't have any @Input in the parent
  2. Run change detection explicitly. You are not doing that.
  3. If an observable linked to the template via the async pipe emits a new value. No sign of the async pipe.
  4. An event originated from the component or one of its children. You are doing this by clicking.

So when you use the http call and click the button it fires the request and runs change detection. But the api call is asynchronous it takes time. By the time it comes back the change detection has already ran and isn't going to run again because none of the 4 criteria above are met.

When you don't use the http call you click the button it changes the value to true and runs change detection. Everything is synchronous so it updates the view straight away.

So your example has nothing to do with the child component. You can change the hello tags to a normal div and stick some letters between the div and you will see the same behavior.

I have updated your StackBlitz with a more reactive approach using the async pipe.

Joselyn answered 3/9, 2020 at 22:16 Comment(0)
A
0

In case of using onPush you should switch to use BehaviorSubject, it happens, because your parent component also has onPush, that's why you should use Subject or manually call detectChanges

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent  {
  constructor(private http: HttpClient) {

  }
  public isHelloVisibleSubject: BehaviorSubject<boolean> = new BehaviorSubject(false);
  public useHttpGet: boolean;

  showHello() {
    if (this.useHttpGet) {
    this.http.get('https://cors-anywhere.herokuapp.com/https://api.darksky.net/forecast/cc0e3799790b0b34bdeb6fef28c3daf7/17.447409200000003,-78.3724573?units=si').subscribe(data => {
      this.isHelloVisibleSubject.next(true);  
    });
    } else {
      this.isHelloVisibleSubject.next(true);
    }
  }

  closeHello() {
    this.isHelloVisibleSubject.next(false);
  }



<hello [showHello]="isHelloVisibleSubject | async"> </hello

Antimacassar answered 10/7, 2020 at 23:12 Comment(2)
I understand what you are saying, but I do not understand why in the original ShowHello() method setting the property outside of the subscribe always works. In both cases the parent and child component have the same restrictions on being set to OnPushSalleysalli
you use onPush in parent component, in your case you have just a variable, that will not force to rerender the view, in my variant I have created Subscription in UI through | async, that's why view will be rerendered everytime, when I update isHelloVisibleSubjectAntimacassar
T
0

When u change ChangeDetectionStrategy.OnPush You components detect changes only when a @input property changes.

in subscribe method you are just changing inner state of component. some how u have to tell angular to detect change so use ChangeDetectorRef.markforcheck()

for more info

and other thing is to use BehaviorSubject and async pipe for change detection.

but in this case better to use first one. it's simple

Tressatressia answered 10/7, 2020 at 23:19 Comment(1)
I am not sure I understand. In the showHello() method I set the property the same way, one is within a subscribe method and one is not. In both I am changing the same property.Salleysalli
M
0

Did you try?

this.close.next(null);
Makeyevka answered 3/12, 2020 at 12:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.