Home > Blockchain >  Angular ExpressionChangedAfterItHasBeenCheckedError only in ngOnInit()
Angular ExpressionChangedAfterItHasBeenCheckedError only in ngOnInit()

Time:11-24

I'm trying to add a loginLoading observable where any component can subscribe to it to find weather a user is currently logging in or not.

In my app.component.html:

<mat-toolbar [ngClass]="{'disable-pointer': loginLoading}">
    <a routerLink="login" routerLinkActive="active" id="login"> Login </a>
</mat-toolbar>

In my app.component.ts:

    public loginLoading;
    constructor(private authenticationService: AuthenticationService) {}

    ngOnInit() {
        this.authenticationService.loginLoading.subscribe(loginLoading => {
            this.loginLoading = loginLoading;
        })
    }

In my login.component.ts:


constructor(private authenticationService: AuthenticationService){}

ngOnInit(): void {
    this.authenticationService.loginLoading.subscribe(loginLoading => this.loginLoading = loginLoading)
    this.authenticationService.login().subscribe(
        data => {
            this.router.navigate([this.state],);
            console.log(`after:  ${this}`)
        },
        error => {
            this.loginFailed = true
            this.error = error.non_field_errors[0]
        });
}

In my AuthenticationService:

private loginLoadingSubject: BehaviorSubject<boolean>;
public loginLoading: Observable<boolean>;


constructor(private http: HttpClient) {
    this.loginLoadingSubject = new BehaviorSubject<boolean>(false);
    this.loginLoading = this.loginLoadingSubject.asObservable();
}


login() {
    this.loginLoadingSubject.next(true)
    return this.http.post<any>(`${environment.apiUrl}/login`, {....})
        .pipe(map(user => {
                .
                .
                this.loginLoadingSubject.next(false)
                .
                .
            }),
            catchError(error => {
                this.loginLoadingSubject.next(false)
                return throwError(error);
            }));
}

Also here is a very simplified example on stackblitz.

My question is why doesn't angular detect the change in the app's component loginLoading field in this line this.loginLoading = loginLoading;? Shouldn't this trigger a change detection cycle?

Also if I move the code in the LoginComponent's ngOnInit() to the LoginComponent's constructor the error does not appear, does this mean that angular checks for changes after the constructor and befor the ngOnInit()?

I solve this by running change detection manually after this line this.loginLoading = loginLoading; in the AppComponent but i'd prefer if i don't or at least know why should I.

CodePudding user response:

for the app component use a async pipe for this case, it will help with change detection.

<mat-toolbar [ngClass]="{'disable-pointer': loginLoading$ | async}">
    <a routerLink="login" routerLinkActive="active" id="login"> Login </a>
</mat-toolbar>

this means you need to store the loading as an observable. however since you have an issue with expressionChanged, i would instead of having a public value in the Auth service i would just return a new one as where needed.

//public loginLoading: Observable<boolean>;

getLoginLoading(): Observable<boolean>{
 return this.loginLoadingSubject.asObservable();
}

this way if timing keeps hitting right between ngOnInit and ngAfterViewInit you can always just set the observable in afterViewInit to avoid the issue.

app.component.ts

   public loginLoading$ : observable<boolean>;
    constructor(private authenticationService: AuthenticationService) {}

    ngOnInit() {
        this.loginLoading$ = this.authenticationService.getLoginLoading();
    }

CodePudding user response:

This happens because you modify Parent state from Child.
Login is child component of app component. With the onInit calling service, the loading flow as below:

  1. App Constructor: appComp starts with loading false due to BehaviorSubject => internally, angular save this as oldValue = false
  2. Login Constructor: None
  3. RenderView: aka update binding. What is the binding in App? it's the loading, and the value of it is false.
    Next is the loginComp binding, no template, we don't care here.
  4. Now run the lifecycle hooks. And because you call the AuthService in onInit. This will run and update the value of loading = true
  5. We've done now. The view is updated in both appComp and LoginComp (AfterViewInit done)

With Developer mode, Angular will run change detection once again to make sure the view is consistent => Now it come to the appComp, compare the oldValue with the currentVal, which now equals true. They are different, which means the view is seeing different values => Wrong, throw Error.

What with the constructor? Why it doesn't throw Error? Here is the flow:

  1. Same. oldValue = false
  2. Login Constructor: Call the authService
  3. RenderView: This time, as the result of authService, loading now = true, oldValue is updated = true

The rest is the same until Angular run change detection the second time, now the oldValue and the currentValue is the same. No error.

What I've been written here is just a gist of the article below, I just add why constructor doesn't trigger error: https://hackernoon.com/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error-e3fd9ce7dbb4

  • Related