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

12.9k views Asked by At

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.

4

There are 4 answers

0
Marcelo Silva On

Did you try?

this.close.next(null);
0
Allan Sham On

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.

2
Slawa Eremin On

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

1
hanan On

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