简体   繁体   中英

ExpressionChangedAfterItHasBeenCheckedError Angular

I'm integrating a loading indicator for my project, the visibility of which will be determined centrally in a HTTP Interceptor service. However, after implementing, I received the following error:

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

Below are my stripped down files:

app.component.ts

import { SpinnerService } from '@app/services/spinner.service';
import { Subject } from 'rxjs';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.scss']
})

export class AppComponent implements OnDestroy {
    title = 'My App';
    isLoading: Subject<boolean> = this.spinnerService.isLoading;

    constructor(public spinnerService: SpinnerService) {
    }

}

app.component.html

<div class="example-container" [class.example-is-mobile]="mobileQuery.matches">
    <router-outlet></router-outlet>
    <div *ngIf="isLoading | async" class="overlay">
        Loading
    </div>
</div>

spinner.service.ts

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable()

export class SpinnerService {

    isLoading = new Subject<boolean>();

    constructor() {
    }

    show() {
        this.isLoading.next(true);
    }

    hide() {
        this.isLoading.next(false);
    }
}

interceptor.service.ts

import { Injectable } from '@angular/core';
import {HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpErrorResponse} from '@angular/common/http';
import {EMPTY, Observable, throwError} from 'rxjs';

import { environment } from '@env/environment';
import { finalize } from 'rxjs/operators';
import { SpinnerService } from '@app/services/spinner.service';

@Injectable()
export class JwtInterceptor implements HttpInterceptor {

    // snackBar: MatSnackBar;

    constructor(public spinnerService: SpinnerService) { }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        this.spinnerService.show();

        return next.handle(request).pipe(
            finalize(() => this.spinnerService.hide()),
        );
    }
}

and finally example usage is below in my other components:

        this.http.get(`${environment.apiUrl}/form/schema/days`)
            .subscribe(data => {
                console.log('done', data);
            });

I can't figure out what I could be doing wrong here, could someone shed some more light on it?

Your loading state should be intiialized (default to false).

As a Subject don't access a value at initialization, you can change it to a BehaviorSubject :

isLoading = new BehaviorSubject<boolean>(false);

Ideally you should call to data sources inside NgOnInit hook, which would prevent this issue. A way to resolve this is by making loader value changes to happen outside of current component initialization cycle using RXJS delay operator:

isLoadingSubject = = new Subject<boolean>();
isLoading: Observable<boolean> = this.isLoadingSubject.pipe( delay(0) );

I think this is a relevant read: https://blog.angular-university.io/angular-debugging/

Updated to unpack a bit on the second line of the code:

Angular features change detection - it is a process that gets triggered by:

  • input events (click, mouseover, keyup, etc.)
  • Ajax requests
  • setTimeout() and setInterval()

It then runs across the components tree of the application checking all the data bindings (subject to CD strategy etc).

The error you see happens when within the same change detection cycle Angular detects that a binding value changed already AFTER the check for changes was performed for that binding, thus causing the issue for Angular as it won't be able to update the template accordingly.

So to fix such an issue we could either ensure the change happens before that specific CD cycle starts (sometimes we can achieve that by moving data changing action to different component lifecycle hook) or by delaying it - making sure the change will be performed in the next cycle.

RXJS's delay under the hood uses setTimeout() which as mentioned above does trigger another CD cycle of its own. Subject is an Observable that can multicast to multiple Observers. So we pipe it to feature a "delay" and thus we reserve a CD cycle for that specific change.

So if we would be pedantic - this is more of a workaround that works. But at the expense of another CD cycle that we run here.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM