Angular 4 - "Expression changed after validation" when using NG-IF

I configure service to track registered users. This service returns the Observable and all components that subscribe to it are notified (so far only one component subscribes to it).

Services:

 private subject = new Subject<any>(); sendMessage(message: boolean) { this.subject.next( message ); } getMessage(): Observable<any> { return this.subject.asObservable(); } 

Root application component: (this component subscribes to observable)

 ngAfterViewInit(){ this.subscription = this._authService.getMessage().subscribe(message => { this.user = message; }); } 

Welcome component:

 ngOnInit() { const checkStatus = this._authService.checkUserStatus(); this._authService.sendMessage(checkStatus); } 

Html application component: (error occurs here)

 <div *ngIf="user"><div> 

What am I trying to do:

I want each component (with the exception of the root application component) to send the registered user state to the root application component, so I can manipulate the user interface in the component component of the root application.

Problem:

I get the following error when the Welcome component is initialized.

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

Note that this error occurs in this expression *ngIf="user" , which is located in the HTML file of the root application components.

Can someone explain the cause of this error and how can I fix it?

On the side of the note: If you think that the best way to achieve what I am trying to do, please let me know.

Update 1:

Entering the following into the constructor solves the problem, but does not want to use the constructor for this purpose, so it seems that this is not a good solution.

Welcome component:

 constructor(private _authService: AuthenticationService) { const checkStatus = this._authService.checkUserStatus(); this._authService.sendMessage(checkStatus); } 

Root Application Component:

 constructor(private _authService: AuthenticationService){ this.subscription = this._authService.getMessage().subscribe(message => { this.usr = message; }); } 

Update 2:

Here is the plunkr . To see the error, check your browser console. When the application loads a boolean true should be displayed, but I get an error in the console.

Please note that this plunkr is a very simple version of my main application. Since the application is a bit large, I was not able to download all the code. But plunkr demonstrates a mistake perfectly.

+8
javascript angular typescript rxjs
source share
5 answers

This means that the change detection cycle itself caused a change that may have been random (i.e. the change detection cycle caused it somehow) or intentionally. If you intentionally change something in a change detection cycle, this should repeat a new round of change detection, which is not happening here. This error will be suppressed in prod mode, but means that you have code problems and cause mysterious problems.

In this case, the special problem is that you are changing something in the child change detection cycle, which affects the parent, and this will not lead to the completion of the detection of parent changes again, even if asynchronous triggers like the ones that are normally executed are executed. The reason that he does not restart the parent cycle is because it disrupts the unidirectional data stream and may create a situation where the child restarts the parent change detection cycle, which then restarts the child and then the parent again and so on and causes an endless detection cycle changes in your application.

It might seem like I'm saying that the child cannot send messages to the parent component, but it is not, the problem is that the child cannot send messages to the parents while detecting changes (for example, life cycle hooks), this should happen outside, like and in the case of a custom event.

The best solution here is to stop breaking the unidirectional data stream by creating a new component that is not the parent of the component causing the update, so the loop for creating an infinite change cannot be created. This is demonstrated below.

New app.component with added child:

 <div class="col-sm-8 col-sm-offset-2"> <app-message></app-message> <router-outlet></router-outlet> </div> 

:

 @Component({ moduleId: module.id, selector: 'app-message', templateUrl: 'message.component.html' }) export class MessageComponent implements OnInit { message$: Observable<any>; constructor(private messageService: MessageService) { } ngOnInit(){ this.message$ = this.messageService.message$; } } 

template:

 <div *ngIf="message$ | async as message" class="alert alert-success">{{message}}</div> 

slightly modified messaging service (only a slightly cleaner structure):

 @Injectable() export class MessageService { private subject = new Subject<any>(); message$: Observable<any> = this.subject.asObservable(); sendMessage(message: string) { console.log('send message'); this.subject.next(message); } clearMessage() { this.subject.next(); } } 

This provides more benefits than just letting the change work properly, without the risk of creating endless loops. It also makes your code more modular and provides better responsibility.

https://plnkr.co/edit/4Th7m0Liovfgd1Z3ECWh?p=preview

+5
source share

Declare this line in the constructor

 private cd: ChangeDetectorRef 

after that in ngAfterviewInit call this

 ngAfterViewInit() { // it must be last line this.cd.detectChanges(); } 

it will solve your problem since the boolean value of the DOM element will not get changed. therefore his throw exception

Your Plunkr answer is here. Please contact AppComponent

 import { AfterViewInit, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Subscription } from 'rxjs/Subscription'; import { MessageService } from './_services/index'; @Component({ moduleId: module.id, selector: 'app', templateUrl: 'app.component.html' }) export class AppComponent implements OnDestroy, OnInit { message: any = false; subscription: Subscription; constructor(private messageService: MessageService,private cd: ChangeDetectorRef) { // subscribe to home component messages //this.subscription = this.messageService.getMessage().subscribe(message => { this.message = message; }); } ngOnInit(){ this.subscription = this.messageService.getMessage().subscribe(message =>{ this.message = message console.log(this.message); this.cd.detectChanges(); }); } ngOnDestroy() { // unsubscribe to ensure no memory leaks this.subscription.unsubscribe(); } } 
+1
source share

Good question, so what causes the problem? What is the cause of this error? We need to understand how the Angular change detection function works, I will briefly explain:

  • You are associating a property with a component
  • You run the application
  • An event occurs (timeouts, ajax calls, DOM events, ...)
  • A related property changes as an event effect.
  • Angular also listens for the event and launches a round of change detection
  • Angular updates view
  • Angular calls ngOnInit , ngOnChanges and ngDoCheck
  • Angular run change detection circle in all child components
  • Angular calls ngAfterViewInit lifecycle hooks

But what if the lifecycle hook contains code that changes the property again and the change detection round does not start? Or what if the lifecycle hook contains code that triggers another round of change detection and the code enters the loop? This is a dangerous case, and Angular does not allow him to pay attention to a property that does not change at the moment or immediately after. This is achieved by using the second round of detecting changes after the first, to be sure that nothing has changed. Please note: this only happens in development mode.

If you fire two events at the same time (or in a very short time frame), Angular will run two change detection cycles at the same time, and in this case there is no problem, because Angular, since both events cause a round of change detection and Angular are smart enough to understand that going on.

But not all events trigger a change detection circle , and your example is an example: Observable does not start a change detection strategy.

What you need to do is awaken Angular to launch the change detection round. You can use the EventEmitter timeout, regardless of what triggers the event.

My favorite solution is using window.setTimeout :

 this.subscription = this._authService.getMessage().subscribe(message => window.setTimeout(() => this.usr = message, 0)); 

This solves the problem.

+1
source share

To understand the error, read:

You fall into the category of Synchronous event broadcasting :

This pattern is illustrated by this plunger . The application is designed to have a child component that emits an event, and a parent component that listens for this event. The event calls the portion of the parent property to be updated. And these properties are used as input binding for the child component . This is also an indirect parent property update.

In your case, the updated property of the parent component of user , and this property is used as binding input to *ngIf="user" . The problem is that you are triggering the this._authService.sendMessage(checkStatus) event as part of the change detection loop, because you are doing this from the lifecycle loop.

As explained in the article, you have two general approaches to circumvent this error:

  • Asynchronous update - this allows you to trigger an event outside the change detection process.
  • Force change detection - this adds an extra change detection run between the current run and the validation phase

First you must answer the question of whether there is a need to call an even from the hook of the life cycle. If you have all the necessary information for an even component in the constructor, I do not think this is a bad option. See the Major Difference Between Constructor and ngOnInit in Angular for more details.

In your case, I would most likely go with asynchronously triggering events instead of manually detecting changes to avoid cycles of detecting redundant changes:

 ngOnInit() { const checkStatus = this._authService.checkUserStatus(); Promise.resolve(null).then(() => this._authService.sendMessage(checkStatus);); } 

or with asynchronous event handling inside the AppComponent:

 ngAfterViewInit(){ this.subscription = this._authService.getMessage().subscribe(Promise.resolve(null).then((value) => this.user = message)); 

The approach shown above is used by ngModel in the implementation .

But I'm also wondering why this._authService.checkUserStatus() is synchronous?

0
source share

I recently encountered the same problem after switching to Angular 4.x, a quick solution is to wrap every piece of code that calls ChangeDetection in setTimeout(() => {}, 0) // notice the 0 it intentional .

Thus, it will click emit AFTER the life-cycle hook , therefore, not cause a change detection error. Although I know this is a rather dirty solution, it is a viable quickfix .

0
source share

All Articles