preface

Presents a lot of people are doing the development of the time, almost all met ExpressionChangedAfterItHasBeenCheckedError this question, you may not understand why will produce the error, Many developers believe that this is a bug in the Angular framework, because this problem only occurs during development, not online. It is an Angular alert mechanism. Prevents errors or outdated data on the page from being displayed to users due to inconsistency between model data and view UI. Next, let’s take a closer look at this warning mechanism

Why does this error occur?

Color {#13c078}{change detection} Angular implements a one-way data flow from top to bottom. After the parent component’s changes have been synchronized, The child component is not allowed to update the parent component’s properties, ensuring that the entire component tree is stable after the first Digest loop.

Specific change detection steps

A running Angular application is actually a tree of components, and during change detection, Angular checks each component in the following order:

  • Update the binding properties of all child components/directives
  • Call ngOnInit, OnChanges, ngDoCheck of all child components/directives
  • Update the DOM of the current component
  • Perform change detection for child components (same three steps above)
  • Call the ngAfterViewInit lifecycle function for all child components/directives

After each action, Angular writes down the values needed to perform the current action and stores them in the oldValues property of the component view. Instead of following up on the list immediately after all components have checked for updates, Angular begins the next Digest Cycle. That is, Angular compares a value from the previous Digest cycle to the current value as follows:

These checks are only performed in the development environment \color{#13c078}{development environment}

  • Checks that the value that has been passed to the child component to update its properties is the same as the value that is currently being passed in
  • Check that the DOM value that has been passed to the current component to update is the same as the value that is currently being passed in
  • The same checks are performed for each child component

Common scenarios that lead to this error

Attribute value mutation

The main culprit for attribute value mutations is a child component or instruction. Let’s look at a simple proof example. You probably know that child components or directives can inject their parent, assuming child B injects its parent, A, and then updates the binding property text. We update parent component A’s properties in the ngOnInit lifecycle hook of child component B. This is because the ngOnInit lifecycle hook will fire after the property binding is complete, causing oldValue to be different from curValue. All consoles will display this error

 // A component (parent component)
 
 @Component({
 selector: 'my-app'.template: ` {{ name }} 
       `.styleUrls: ['./app.component.css']})export class AComponent {
 name = 'I am A component';
 text = 'A message for the child component';
 }
 
 
 // B component (subcomponent)
 
 @Component({
 selector: 'b-comp'.template: ' I am B component '
})
export class BComponent {
 @Input() text;

 constructor(private parent: AppComponent) {}

 ngOnInit() {
   this.parent.text = 'updated text'; }}Copy the code

Synchronous event broadcast

When a program is designed to throw an event for a child component that the parent listens for, the event causes the parent component’s property value to change. At the same time, these property values are bound by the parent component to the child component as input properties. This is also an indirect update of the parent component’s property, which is actually caused by the mutation of the property. We also use A and B components to slightly restore this scenario

 // A component (parent component)
 
 @Component({
  selector: 'my-app'.template: ` {{ name }} 
       `.styleUrls: ['./app.component.css']})export class AppComponent implements OnInit.AfterViewInit {
  name = 'I am A component';
  text = 'A message for the child component';
  constructor(private cd: ChangeDetectorRef) {}

  update(value) {
    this.text = value; }}// B component (subcomponent)
 
 @Component({
  selector: 'b-comp'.template: ` {{ name }} `
})
export class CComponent {
  name = 'I am B component';
  @Input() text;
  @Output() change = new EventEmitter();

  constructor() {}

  ngOnInit() {
    this.change.emit('updated text'); }}Copy the code

Dynamic component instantiation

When a parent component dynamically adds a child component to the ngAfterViewInit lifecycle hook. Because adding child components triggers DOM changes, and the ngAfterViewInit lifecycle hook is also triggered after DOM updates, an error is also thrown. As above, we still use A, B components simple implementation.

// A component (parent component)

@Component({
  selector: 'my-app'.template: ` {{ name }} 
       `.styleUrls: ['./app.component.css']})export class AppComponent implements AfterViewInit {
  @ViewChild('vc', {read: ViewContainerRef}) vc;
  name = 'I am A component';

  constructor(private r: ComponentFactoryResolver) {}

  ngAfterViewInit() {
      const f = this.r.resolveComponentFactory(BComponent);
      this.vc.createComponent(f);
  }


// B component (subcomponent)

@Component({
  selector: 'b-comp'.template: ` {{ name }} `
})
export class DComponent {
  name = 'I am B component';
}

Copy the code

Shared services

But we have a shared service in the program, and the child component modifies a property value of the shared service, causing the property value of the parent component to change in response. I call this an indirect parent component property update, and the same warning error is reported as in the previous example.

How to resolve this error?

Asynchronous update

Because Angular’s change-detection and verification digests are synchronous, this means that if we update attribute values asynchronously while the verification loop is running, there is no error. The related changes are as follows:

  // in the B component
  @Component({
  selector: 'b-comp'.template: ' I am B component '
})
export class BComponent {
  @Input() text;

  constructor(private parent: AppComponent) {}

  ngOnInit() {
  // The first method
    setTimeout(() = > {
      this.parent.text = 'updated text';
    });
    
    // The second method
    Promise.resolve(null).then(() = > {
      this.parent.text = 'updated text'; }); }}Copy the code

Forced change detection

Another solution is to force Angular to perform change detection for parent COMPONENT A again between the first change detection and verification cycle phases. The best time to trigger parent A’s change detection is in the ngAfterViewInit hook, because the parent’s hook function will be triggered after all the children have done their own change detection, and it is the children doing their own change detection that may change the parent property value.

/ / A component
export class AppComponent implements AfterViewInit {
  name = 'I am A component';
  text = 'A message for the child component';
  constructor(private cd: ChangeDetectorRef) {}

  ngAfterViewInit() {
    this.cd.detectChanges(); }}Copy the code

But this approach is not very good. If we trigger change detection for parent A, Angular will still trigger change detection for all of its children, which may cause the parent property values to change again.

conclusion

In fact, neither of the above solutions is particularly recommended. We encountered this problem in development. The better solution is to redesign your application to avoid such errors. Because Angular protects us from building programs that are hard to maintain over the long term by throwing the error “expression changed after checking” (only in development mode). Although a bit unexpected at first glance, this error is very helpful, and Angular implements one-way data flow from top to bottom. It doesn’t allow children to update the parent’s properties after the parent’s changes have been synchronized, ensuring that the entire component tree is stable after the first Digest loop. If the value of the properties changes, then the consumers (that is, the child components) that depend on those properties need to be synchronized, leading to instability in the component tree. In our example, child component B depends on the parent’s Text property, and whenever the text property changes, the entire component tree is unstable unless it has already been passed to component B. The same is true for the DOM template in parent COMPONENT A, which is the consumer of the attributes in model A and renders the data in the UI. If these attributes are not synchronized in time, the user will see the wrong data information on the page.