1

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.

Edit: I understand that in development mode, angular checks the model didn't change using 1 extra check. What I assumed would happen is since an observable firing a new value would trigger a new change detection cycle the error shouldn't appear.

To my understanding that if an observable fire between the 2 checks (and it's value is bound to the view), angular wouldn't know (and wouldn't trigger change detection again) and therefore the error appears after the second check

  • Does this answer your question? [Expression \_\_\_ has changed after it was checked](https://stackoverflow.com/questions/34364880/expression-has-changed-after-it-was-checked) – Vikas Nov 24 '22 at 03:44
  • No it doesn't, it offers a solution to remove the error but still does not explain parts i don't understand about change detection. – Mohamed Kemega Nov 24 '22 at 20:51

3 Answers3

0

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();
    }
  • This results in the same error: https://stackblitz.com/edit/angular-ivy-44jtid?file=src/app/app.component.ts You are still modifying parent state during change detection. – Chris Hamilton Dec 01 '22 at 02:07
0

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

Jimmy
  • 1,276
  • 7
  • 8
  • Thanks for the explanation, Does this mean that the error occurs when a child component changes the parent component after RenderView? and does this also mean that angular won't check for changes when the observable is fired after RenderView? – Mohamed Kemega Nov 26 '22 at 00:00
  • hmm, you should really read the link ViKas and I gave above. It explains very well. The "RenderView" isn't the main thing. The main thing here is Angular will run Change Detection twice, and if the values between 2 times ain't the same, it will throw error. – Jimmy Nov 26 '22 at 18:18
0

The change after checked error is there to stop you from changing things higher up the component tree during change detection, which can lead to the UI not being updated, and therefore being out of sync with the component.

Change detection cascades down the tree, and will not backtrack to update UI after it has passed a component. So having a child modify parent state during change detection de-syncs the UI. The double check and throwing an error exists in development mode to catch these UI out of sync bugs from happening in production. This is just sort of a janky side effect from using a hierarchical change detection system (in my opinion).

In this case it involves the loading property of your top level component. You travel down the component tree to the login component, which indirectly changes the loading property of your top level component during that initial round of change detection.

You can just push the property change to after change detection runs by pushing the task further down the event loop. This can be done using a zero delay setTimeout or queueMicrotask https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#zero_delays.

  ngOnInit(): void {
    queueMicrotask(()=>this.authenticationService.login()); // no error
  }

demo: https://stackblitz.com/edit/angular-ivy-wubv1q?file=src/app/login/login.component.ts

or you can turn the login function itself into a microtask

  login() {
    queueMicrotask(() => {
      this.loginLoadingSubject.next(true);
      setTimeout(() => {
        this.loginLoadingSubject.next(false);
      }, 500);
    });
  }
  ngOnInit(): void {
    this.authenticationService.login() // no error
  }

demo: https://stackblitz.com/edit/angular-ivy-kovyo7?file=src/app/_services/authentication.service.ts


As for the error not appearing when you put the call in the constructor: the component is instantiated as an object before being attached to the component tree and running change detection / lifecycle hooks, so yes the constructor is run before change detection. There's nothing wrong with putting the call in the constructor for this purpose.

The same is true with property initializers, for example this would also avoid the error, but it doesn't make sense for a void return value:

export class LoginComponent {
  _ = this.authenticationService.login(); // no error
  constructor(private authenticationService: AuthenticationService) {}

demo: https://stackblitz.com/edit/angular-ivy-51u7ju?file=src/app/login/login.component.ts


You may also want to just rethink your design, if you are attempting to login immediately, could you just do it in the service rather than a component?

export class AuthenticationService {
  private loginLoadingSubject = new BehaviorSubject<boolean>(true);
  public loginLoading = this.loginLoadingSubject.asObservable();

  constructor() {
    this.login();
  }

  login() {
    setTimeout(() => {
      this.loginLoadingSubject.next(false);
    }, 500);
  }
}

demo: https://stackblitz.com/edit/angular-ivy-vumd1c?file=src/app/_services/authentication.service.ts

this constructor will trigger during the first instance of injecting the service. It will be executed before change detection just like component constructors and property initializers.

Chris Hamilton
  • 9,252
  • 1
  • 9
  • 26