Search code examples
angular8angular-http-interceptorsangular-changedetection

ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked in HTTP loading interceptor


Goal:

Im trying to show a loading icon on every ajax call.To this end, I added an HTTP interceptor which sets a variable to true when one or more requests are ongoing and to false when all have completed. The UI tests for this value and shows a loader or not, depending.

Problem:

On every ajax call, an error is thrown:

ExpressionChangedAfterItHasBeenCheckedError: 
Expression has changed after it was checked. 
Previous value: 'ngIf: [object Object]'. Current value: 'ngIf: true'.

enter image description here

Simplified Stakckblitz with reproducible error:

https://stackblitz.com/edit/angular-h4rpfb

Code:

appcomponent.html:

<p *ngIf="loaderService.isLoading | async">
  Loading!
</p>
<p *ngIf="!(loaderService.isLoading | async)">
  Not Loading!
</p>
<button (click)="loadSomething()">Load Something</button>
{{matches|async}}

appcomponent.ts:

import { Component } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { LoaderService } from "./core";
import { Observable } from "rxjs";

@Component({
  selector: "my-app",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent {
  matches: Observable<any>;

  constructor(public loaderService: LoaderService, private http: HttpClient) {}

  loadSomething() {
    this.matches = this.http.get("https://jsonplaceholder.typicode.com/posts");
  }
}

loader.interceptor.ts:

import { Injectable } from '@angular/core';
import {
  HttpErrorResponse,
  HttpResponse,
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { LoaderService } from './loader.service';

@Injectable()
export class LoaderInterceptor implements HttpInterceptor {
  private requests: HttpRequest<any>[] = [];

  constructor(private loaderService: LoaderService) { }

  removeRequest(req: HttpRequest<any>) {
    const i = this.requests.indexOf(req);
    if (i >= 0) {
      this.requests.splice(i, 1);

    }
    console.log(i, this.requests.length);
    this.loaderService.isLoading.next(this.requests.length > 0);
  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    this.requests.push(req);
    this.loaderService.isLoading.next(true);
    return Observable.create(observer => {
      const subscription = next.handle(req)
        .subscribe(
        event => {
          if (event instanceof HttpResponse) {
            this.removeRequest(req);
            observer.next(event);
          }
        },
        err => { this.removeRequest(req); observer.error(err); },
        () => { this.removeRequest(req); observer.complete(); });
      // teardown logic in case of cancelled requests
      return () => {
        this.removeRequest(req);
        subscription.unsubscribe();
      };
    });
  }
}

loader.service.ts:

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { ReplaySubject } from 'rxjs/ReplaySubject';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';


@Injectable()
export class LoaderService {
    public isLoading = new BehaviorSubject(false);

    constructor() {}
}

Solution

  • Ok I got it to work by adding this to the component with the loader:

    changeDetection: ChangeDetectionStrategy.OnPush
    

    So the appcomponent.html now looks like this:

    import { Component,ChangeDetectionStrategy  } from "@angular/core";
    import { HttpClient } from "@angular/common/http";
    import { LoaderService } from "./core";
    import { Observable } from "rxjs";
    
    @Component({
      changeDetection: ChangeDetectionStrategy.OnPush,
      selector: "my-app",
      templateUrl: "./app.component.html",
      styleUrls: ["./app.component.css"]
    })
    export class AppComponent {
      matches: Observable<any>;
    
      constructor(public loaderService: LoaderService, private http: HttpClient) {}
    
      loadSomething() {
        this.matches = this.http.get("https://jsonplaceholder.typicode.com/posts");
      }
    }
    

    Example:

    https://stackblitz.com/edit/angular-n6fzjm