Angular 4 - “Expression has changed after it was checked” error while using NG-IF

后端 未结 6 1528
天命终不由人
天命终不由人 2020-12-14 21:05

I setup a service to keep track of logged in users. That service returns an Observable and all components that subscribe to it are notified (so

相关标签:
6条回答
  • 2020-12-14 21:15

    Declare this line in constructor

    private cd: ChangeDetectorRef
    

    after that in ngAfterviewInit call like this

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

    it will resolve your issue because DOM element boolean value doesnt get change. so its throw exception

    Your Plunkr Answer Here Please check with 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();
        }
    }
    
    0 讨论(0)
  • 2020-12-14 21:15

    Nice question, so, what causes the problem? What's the reason for this error? We need to understand how Angular change detection works, I'm gonna explain briefly:

    • You bind a property to a component
    • You run an application
    • An event occurs (timeouts, ajax calls, DOM events, ...)
    • The bound property is changed as an effect of the event
    • Angular also listens to the event and runs a Change Detection Round
    • Angular updates the view
    • Angular calls the lifecycle hooks ngOnInit, ngOnChanges and ngDoCheck
    • Angular run a Change Detection Round in all the children components
    • Angular calls the lifecycle hooks ngAfterViewInit

    But what if a lifecycle hook contains a code that changes the property again, and a Change Detection Round isn't run? Or what if a lifecycle hook contains a code that causes another Change Detection Round and the code enters into a loop? This is a dangerous eventuality and Angular prevents it paying attention to the property to don't change in the while or immediately after. This is achieved performing a second Change Detection Round after the first, to be sure that nothing is changed. Pay attention: this happens only in development mode.

    If you trigger two events at the same time (or in a very small time frame), Angular will fire two Change Detection Cycles at the same time and there are no problems in this case, because Angular since both the events trigger a Change Detection Round and Angular is intelligent enough to understand what's happening.

    But not all the events cause a Change Detection Round, and yours is an example: an Observable does not trigger the change detection strategy.

    What you have to do is to awake Angular triggering a round of change detection. You can use an EventEmitter, a timeout, whatever causes an 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.

    0 讨论(0)
  • 2020-12-14 21:18

    I recently encountered the same issue after migration to Angular 4.x, a quick solution is to wrap each part of the code which causes the ChangeDetection in setTimeout(() => {}, 0) // notice the 0 it's intentional.

    This way it will push the emit AFTER the life-cycle hook therefore not cause change detection error. While I am aware this is a pretty dirty solution it's a viable quickfix.

    0 讨论(0)
  • 2020-12-14 21:22

    To understand the error, read:

    • Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error

    You case falls under the Synchronous event broadcasting category:

    This pattern is illustrated by this plunker. The application is designed to have a child component emitting an event and a parent component listening to this event. The event causes some of the parent properties 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 parent component property that is updated is user and this property is used as input binding to *ngIf="user". The problem is that you're triggering an event this._authService.sendMessage(checkStatus) as part of change detection cycle because you're doing it from lifecycle hook.

    As explained in the article you have two general approaches to working around this error:

    • Asynchronous update - this allows triggering an event outside of change detection process
    • Forcing change detection - this adds additional change detection run between the current run and the verification stage

    First you have to answer the question if there's any need to trigger the even from the lifecycle hook. If you have all the information you need for the even in the component constructor I don't think that's the bad option. See The essential difference between Constructor and ngOnInit in Angular for more details.

    In your case I would probably go with either asynchronous event triggering instead of manual change detection to avoid redundant change detection cycles:

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

    or with asynchronous event processing inside the AppComponent:

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

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

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

    0 讨论(0)
  • 2020-12-14 21:34

    What this means is that the change detection cycle itself seems to have caused a change, which may have been accidental (ie the change detection cycle caused it somehow) or intentional. If you do change something in a change detection cycle on purpose, then this should retrigger a new round of change detection, which is not happening here. This error will be suppressed in prod mode, but means you have issues in your code and cause mysterious issues.

    In this case, the specific issue is that you're changing something in a child's change detection cycle which affects the parent, and this will not retrigger the parent's change detection even though asynchronous triggers like observables usually do. The reason it doesn't retrigger the parent's cycle is becasue this violates unidirectional data flow, and could create a situation where a child retriggers a parent change detection cycle, which then retriggers the child, and then the parent again and so on, and causes an infinite change detection loop in your app.

    It might sound like I'm saying that a child can't send messages to a parent component, but this is not the case, the issue is that a child can't send a message to a parent during a change detection cycle (such as life cycle hooks), it needs to happen outside, as in in response to a user event.

    The best solution here is to stop violating unidirectional data flow by creating a new component that is not a parent of the component causing the update so that an infinite change detection loop cannot be created. This is demonstrated in the plunkr below.

    New app.component with child added:

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

    message component:

    @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 message service (just 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 has more benefits than just letting change detection work properly with no risk of creating infinite loops. It also makes your code more modular and isolates responsibility better.

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

    0 讨论(0)
  • 2020-12-14 21:35

    Don't change the var in ngOnInit, change it in constructor

    constructor(private apiService: ApiService) {
     this.apiService.navbarVisible(false);
    }
    
    0 讨论(0)
提交回复
热议问题