Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ExpressionChangedAfterItHasBeenCheckedError on Angular 8 and method called on ngOnInit

I'm facing some common issue on Angular template, I have this general template for all my pages and inside of it I have a *ngIf with a spinner inside and another one with my router-outlet.

The interceptor controlls the behavior and visibility of the spinner, so with that comes the error, in a specific component I subscribe on a http method on a ngOnInit and the result of that is the error.

This error just happen if I call some method on lifecycle methods. I already tried some workarounds like envolve the setSpinnerState with setTimeout and tried to use other lifecycle hooks(AfterViewInit, AfterContentInit...).

The interceptor

export class InterceptorService implements HttpInterceptor {

  intercept(req: HttpRequest<any>, next: HttpHandler): import('rxjs').Observable<HttpEvent<any>> {
    this.layoutService.setSpinnerState(true);
    return next.handle(this.handlerRequest(req)).pipe(
      delay(3000),
      finalize(() => {
        this.layoutService.setSpinnerState(false);
      })
    );
  }

  constructor(private authService: AuthService, private layoutService: LayoutService) { }

  private handlerRequest(req: HttpRequest<any>) {
    let request;
    if (this.authService.isLoggedIn() && !req.url.includes('/assets/i18n/')) {
      request = req.clone({
        url: environment.api.invokeUrl + req.url,
        headers: req.headers.append('Authorization', 'Bearer ' + this.authService.getAcessToken())
      });
    } else if (req.url.includes('/assets/i18n/')) {
      request = req.clone();
    } else {
      request = req.clone({
        url: environment.api.invokeUrl + req.url
      });
    }
    return request;
  }

}

Layout Service:

export class LayoutService {

  private isSpinner: boolean = true;

  constructor() { }

  public setSpinnerState(state: boolean): void {
    this.isSpinner = state;
  }

  public getSpinnerState():boolean{
    return this.isSpinner;
  }


}

The general template(LayoutComponent)

<div class="page-wrapper fixed-nav-layout">
    <app-header (toggleEvent)="receiveToggle($event)"></app-header>
    <!--Page Body Start-->
    <div class="container-fluid">
        <div class="page-body-wrapper sidebar-icon" [class.sidebar-close]="toggle">
            <app-sidebar class="page-sidebar page-sidebar-open"></app-sidebar>
            <div class="page-body p-t-10">
                <app-breadcrumb></app-breadcrumb>
                <router-outlet  *ngIf="!layoutService.getSpinnerState()"></router-outlet>
                <div class="loader-box d-flex justify-content-center align-self-center" *ngIf="layoutService.getSpinnerState()" async>
                    <div class="loader">
                        <div class="line bg-default"></div>
                        <div class="line bg-default"></div>
                        <div class="line bg-default"></div>
                        <div class="line bg-default"></div>
                    </div>
                </div>
            </div>
        </div>
        <!--Page Body End-->
    </div>

The component with the subscribe method

export class EditProfileComponent implements OnInit {

  public userProfile = new UserProfile();

  constructor(private userService: UserService) {
  }

  ngOnInit() {
    this.getUser();
  }

  public getUser() {
    // debugger
    this.userService.getUser().subscribe(data => {
      this.userProfile = Amazon.feedUser(data);
    }, err => {
      return new ErrorHandler().show(err.message);
    });
  }
}

And the error stack

ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'ngIf: true'. Current value: 'ngIf: false'.
    at viewDebugError (core.js:28792)
    at expressionChangedAfterItHasBeenCheckedError (core.js:28769)
    at checkBindingNoChanges (core.js:29757)
    at checkNoChangesNodeInline (core.js:44442)
    at checkNoChangesNode (core.js:44415)
    at debugCheckNoChangesNode (core.js:45376)
    at debugCheckDirectivesFn (core.js:45273)
    at Object.eval [as updateDirectives] (LayoutsComponent.html:10)
    at Object.debugUpdateDirectives [as updateDirectives] (core.js:45258)
    at checkNoChangesView (core.js:44248)

At last I tried ChangeDetectorRef on LayoutComponent, but error doesn't changed.

export class LayoutsComponent implements OnChanges {

  public toggle;
  openToggle: boolean;

  constructor(public navService: NavService, public layoutService: LayoutService, private cd: ChangeDetectorRef) {
    if (this.navService.openToggle === true) {
      this.openToggle = !this.openToggle;
      this.toggle = this.openToggle;
    }
  }

  ngOnChanges(): void {
    this.cd.detectChanges();
  }

  receiveToggle($event) {
    this.openToggle = $event;
    this.toggle = this.openToggle;
  }

}

The error reproduced on stackblitz: https://stackblitz.com/edit/angular-xyulo9

like image 436
Gabriela Mendes Avatar asked Oct 07 '19 20:10

Gabriela Mendes


People also ask

How do I fix error ExpressionChangedAfterItHasBeenCheckedError?

A good way to fix the ExpressionChangedAfterItHasBeenCheckedError is to make Angular aware of the fact that you have changed the code after it has completed its first check. You can do this by using setTimeout to make the error go away.

How do I fix ng0100 error?

If the issue exists within ngAfterViewInit , the recommended solution is to use a constructor or ngOnInit to set initial values, or use ngAfterContentInit for other value bindings. If you are binding to methods in the view, ensure that the invocation does not update any of the other bindings in the template.


1 Answers

Original Stackblitz*

ExpressionChangedAfterItHasBeenCheckedError is a development check to ensure bindings update in a manner Angular expects. The order of change detection when it run is as follows:

  1. OnInit, DoCheck, OnChanges
  2. Renders
  3. Change detection runs on child views
  4. AfterViewChecked, AfterViewInit

A final development check makes sure there are no changes after having been checked (else all the children would need to be updated).

The problem is that layoutService.getSpinnerState() is changed by the child after the parent has determined and "rendered" this.

Stackblitz - A Solution

The problem you have boils down to

  1. Where should you call getUser()?

    For the sake of simplicity I've called this in AppComponent.

    If you call this in EditProfileComponent OnInit this will trigger an infinite loop.

    spinner is false, Parent renders Child, Child calls getUser(), spinner set to true, Parent removes Child and with the http response we repeat.

  2. How to pass data to EditProfileComponent?

    For the sake of simplicity I've created a PassDataService to hold the data and then pass this along on EditProfileComponent OnInit.

Alternatives include using a guard or resolver or allowing EditProfileComponent to render but having the spinner overlay this until the http response.

Unidirectional Data Flow

AppComponent → calls getUser() with data saved in service → sets spinnerState true → renders spinner

On recieving data:

interceptor → sets spinnerState false → renders router-outlet → renders EditProfileComponent → data passed from service


*This is the same as the original stackblitz but with logging added to give an idea of change detection and the lifecycle hooks as well as putting the template inside the ts file for easier analysis.

like image 168
Andrew Allen Avatar answered Sep 20 '22 06:09

Andrew Allen