Search code examples
angulartypescriptrxjsangular-changedetection

ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked - How to update template after Observable Value change


I went through quite a lot of SO posts trying to find a solution to this one, the only one I found had a hack implementation. I have an observable taken from the ngrx store which I subscribe too:

this.navigationSelected$ = this.store.pipe(select(currentlySelectedNavigation));

this.navigationSelected$.subscribe(res => {
  ...
});

with an ngIf depending on this observable value inside of the template:

<profile-navigation *ngIf="(navigationSelected$ | async) == navigationLayout[0].location"></profile-navigation>

Whenever the value of navigationSelected$ changes, this throws:

ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'ngIf: [object Object]'. Current value: 'ngIf: false'.

and the template does not update. I managed to go around it by running cdRef.detectChanges(); at the end of the subscription. It works fine but the error is still being thrown, plus as mentioned, it seems like a hack.

What would be the best way of achieving what I am trying to do?


Solution

  • Whenever the value of navigationSelected$ changes, this throws:

    The error means that the value changed twice.

    When you have these errors on a selector it can be very difficult to fix them. There isn't really anything wrong with the view. The problem is that the store is changing state before and after the view is rendered, and this likely means that there is a dispatch that should happen after a setTimeout() is called.

    The problem is that this makes some other place in your source code dependent upon changing the state to protect the view from triggering the error. That's not ideal.

    An alternative is to emit the value using EventEmitter.

    <profile-navigation *ngIf="(navigationSelectedSafe$ | async) == navigationLayout[0].location"></profile-navigation>
    
    public navigationSelectedSafe$ = new EventEmitter<any>(true); // must be true
    
    this.navigationSelected$.subscribe(res => navigationSelectedSafe$.emit(res));
    
    

    When you use EventEmitter(true) it will emit values after a setTimeout() which protects the view from change errors.

    You can also search around your source code for places where you use @Output() and see if changing it to EventEmitter(true) fixes the problem.

    Usually when you see this error on a selector. It means that you're doing a lot of state related work outside of the views. A component that needs to broadcast that something has changed should be using @Output() but if that component is dispatching, then it bypasses the view process. This is where you run into these issues.