7
votes

I have a simple Angular 5 application with a HttpInterceptor that notifies a service when a request is made. Components can subscribe to this service to get this information.

Full example with error here => https://stackblitz.com/edit/angular-5b86se

In my AppComponent I want to show a loading indicator when request are made in my child component (HelloComponent).

import { Component } from '@angular/core';
import { LoadingIndicatorService } from './loading-indicator.service';

@Component({
  selector: 'my-app',
  template: `<div *ngIf="loading" style="color:red">pretend this is loading indicator</div>
<hello name="{{ name }}"></hello>`
})
export class AppComponent  {
  name = 'Angular 5';
   loading: boolean = false;

   constructor(private loadingIndicatorService: LoadingIndicatorService) { 

    loadingIndicatorService
      .onLoadingChanged
      .subscribe(isLoading => this.loading = isLoading);
  }
}

I get the following error:

Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'false'. Current value: 'true'.

If I'm correct this error appears when a child component changes a value in a parent component.

But my child component (HelloComponent) is only doing a HttpGet and the HttpInterceptor is changing the parent component (AppComponent) over a service with an observable.

Shouldn't changing over a service be the correct solution here? Can someone explain why this doesn't work?

3

3 Answers

4
votes

It throws an error because your variable loading gets updated when onLoadingChanged is called. This means your initial value got changed.

I would suggest you to read more about here

The general fix for this would be,

    constructor(private loadingIndicatorService: LoadingIndicatorService,private ref: ChangeDetectorRef) { 
    loadingIndicatorService.onLoadingChanged.subscribe(isLoading => {
       this.loading = isLoading
       this.ref.detectChanges();
    });
 }
0
votes

AsyncPipe to the rescue:

<div *ngIf="loadingIndicatorService.onLoadingChanged | async">Loading...</div>

Remember to change the service into public.

Edit: Needed to also set changeDetectionStrategy to OnPush. Working blitz: https://stackblitz.com/edit/angular-ay1ikq

As for why this is happening without OnPush even with AsyncPipe, can't say. Maybe someone with deep knowledge into Angular core? CD triggered internally right before/after interceptors? I almost always use OnPush but there has to be a situation where I didn't at first, and still haven't seen this error happen with AsyncPipe. Only thing really different from anything I've done is using interceptors to do something that causes a change in a value bound to a template, so that's got to be the root cause?

0
votes

The ExpressionChangedAfterItHasBeenCheckedError gets thrown (in developer mode) when an expression changes more than one time during one change detection lifecycle. So how is it changing in your example?

Lifecycle #1:

  • loading: boolean = false; during the constructor of AppComponent
  • The hello component makes the http request in onInit. This then triggers the the interceptor which updates the observable.

Lifecycle #2 (in dev mode, to check whether all values are stable / still the same):

  • loading is now true because the subscription to the observable changed the value, which is different than the previous value of loading = false, so Angular throws the error.

To mitigate the problem, you can:

  • move the code from onInit to the constructor. downside: it's not recommended to do stuff in the constructor
  • include cdr as Sajeetharan describes. downside: you'll have to do this on every client of your observable.
  • emit the changed observable value after a timeout setTimeout(() => myObservable.next(...));. downside: it's ugly. However it might cause you the least trouble if you expect multiple clients to subscribe to your observable, as you don't have to include cdr everywhere.

To be honest, none of those solutions are particularly pretty.

Note that the async pipe has nothing to do with the problem as suggested in another answer.