[Solved] ExpressionChangedAfterItHasBeenCheckedError Error Notes, and Change Detection

Error Notes for ExpressionChangedAfterItHasBeenCheckedError

Related change detection behavior

A running Angular program is a tree of components. During change detection, Angular checks each component in the following order, (see Listing 1 below)

  • Update binding properties of all child components/directives
  • Three declaration cycles that call all child components/directives: OnChanges, ngOnInit, ngDocheck
  • Perform change detection for subcomponents (repeat the above three steps on subcomponents, recursively in turn)
  • Call the ngAfterViewInit declaration cycle hook of the current component for all child components/directives

It can be seen here that the ngAfterViewInit hook is after change detection. Asynchronous operation will trigger Angular’s change detection, so if you want to change the subcomponent properties in the ngAfterViewInit hook, you can put a layer of settmeout into it and become an asynchronous operation
After each operation, Angular will record the values required to perform the current operation and store them in the oldValues attribute of the component view (Angular Compiler will compile each component into the corresponding view class, that is, the component View class) After the check and update operation of all components is completed, Angular does not perform the operations in the above list immediately, but starts the next digest cycle, that is, Angular will combine the value from the previous digest cycle with Current value comparison (do list 2 below)

  • Checks that the value that has been passed to the child component to update its property is the same as the current value to be passed in.
  • Check if the value that has been passed to the current component to update the DOM is the same as the current value to be passed in
  • Perform the same check for each sub-component (if the sub-component has sub-components, the sub-component will continue to perform the above two operations, recursively in turn)

This check is only performed in the development environment.

For example: Suppose there is another parent component A and child component B, and the A component has name and text attributes, and the template expression of the name attribute is used in the A component template:

//Parent component => A component
template: '<span>{<!-- -->{name}}</span>'

At the same time, there is also a B component, and the text property of the parent component of A is passed to the B component in the form of input property binding.

//parent component => parent component
@Component({<!-- -->
    selector: 'a-comp',
    template: `
        <span>{<!-- -->{name}}</span>
        <b-comp [text]="text"></b-comp>
    `
})

export class AComponent {<!-- -->
    name = 'I am A component';
    text = 'A message for the child component`;

What happens if Angular performs change detection
Start by examining the parent component A, according to the behavior listed in Listing 1,

  1. The first step update the binding property of all child components/directives, so Angular will calculate the value of the text expression as A message for the child component, and pass the value down to the child component B, and Angular will also store this value in the current view
    view.oldValues[0] = 'A message for the child component';
    
  2. The second step executes several of the declaration cycle hooks listed in Listing 1 above. (i.e. call onChanges, ngOnInit, ngDoCheck of subcomponent B)
  3. The third part is to calculate the value of the template expression {{name}} as I am A component, and then update the DOM of the current component A, At the same time, Angular will also store this value in the current component view:
    view.oldValues[1] = 'I am A component';
    
  4. The fourth step is to perform the same operations as the following steps 1 to 3 for subcomponent B. Once component B is checked, the digest loop ends. (Because the Angular program is composed of a component tree, the current parent component A component does step 123, and then the subcomponent B will also do step 123. If component B has subcomponent C, it will recursively until you know the current branch. The end, that is, the last component has no children. This process is called digest loop).

If in developer mode, Angular also performs the digest cycle checks listed in Listing 2. Suppose that when component A has passed the value of the text attribute down to component B and saved the value, this is the text value icon as updated text, so that when Angular runs the digest cycle verification, it will execute the list The first step of 2 is to check whether the text attribute value of the current digest cycle has changed from the last text attribute value:

AComponentView.instance.text === view.oldValues[0]; // false
'A message for the child component' === 'updated text'; // false

When the result changes, Angular throws an ExpressionChangedAfterItHasBeenCheckedError error.
The third step in Listing 1 also performs a digest cycle check. If the name attribute has been rendered in the DOM and stored in the component view, then the name attribute value mutation will also have the same error:

AComponentView.instance.name === view.oldValues[1]; // false
'I am A component' === 'updated name'; // false

The reason for the sudden change of attribute value

Property value mutations can be caused by child components or directives.
Take a chestnut:
A child component or directive can be injected into a parent component, let’s say child component B is injected into its parent component A, and then the bound property text is updated. Update the properties of parent component A in the ngOnInit lifecycle hook of child component B, because the ngOnInit lifecycle hook will be triggered after the property binding is completed.

//Subcomponent B
export class BComponent {<!-- -->
    @Input() text;
    constructor(private parent: AppComponent) {<!-- -->}
    ngOnInit() {<!-- -->
        this.parent.text = 'updated text';
    }
}

An error will be reported at this time, if the name attribute of the parent component A is changed at the same time

ngOnInit() {<!-- -->
    this.parent.name = 'updated name';
}

No error is reported. In the operation execution sequence of List 1, it will be found that the ngOnInit life cycle hook will be triggered before the DOM update operation is executed, so no error will be reported

Understanding unidirectional data flow

As mentioned above, each time the change detection is triggered, the change detection of each component will be executed from the root component along the entire component tree from top to bottom. By default, the change detection implemented by the last leaf (Component) component is known. reach a steady state. In the whole process, once the parent component implements change detection, its descendant components are not allowed to change the change detection attribute state of the parent component until the next event triggers the change detection. This is the single-item data flow.

Change Detection Execution Order: Development Mode as an Example

  1. Listing 1 Update binding properties of all child components/directives
  2. Listing 2 Checks whether the value that has been passed to the child component to update its property is the same as the current value to be passed in
  3. Listing 2 Checks whether the value that has been passed to the current component to update the DOM is the same as the current value to be passed in
  4. Listing 2 performs the same check for each subcomponent.
  5. Listing 1 Three lifecycle hooks that call all child components/directives: ngOnInit, OnChanges, ngDoCheck
  6. Listing 1 performs change detection for subcomponents
  7. Listing 1 calls the current component’s ngAfterViewInit lifecycle hook for all child components/directives

In other words, if you change the properties of the child component in the parent component, because the change detection is a single data flow, it will prevent the parent component view from being updated in the same cycle, you can wait for the next round by adding settimeout.

  1. Receive the Input parameter sent by the parent node, and save this Input parameter as oldInput
  2. Call your own declaration cycle function in the order of ngOnChanges, ngOnInit, ngDoCheck
  3. If there are subcomponents, download the Input parameters of the subcomponents, and call the ngOnChanges, ngOnInit, and ngDoCheck parameters of the subcomponents in sequence
  4. Do change detection by yourself, update your own DOM structure at the same time, save the DOM structure at this time, record it as oldDom
  5. Subcomponents perform change detection and update the dom structure at the same time
  6. Child component calls ngAfterViewInit
  7. Call ngAfterViewInit by itself
  8. If it is development mode, a second cycle will be performed, repeating 1-7
  9. Steps 1 and 4 of the second round of the loop will check whether oldInput is equal to input or whether oldDom is equal to oldDom. If it is different, ExpressionChangedAfterItHasBeenCheckedError will be reported.

How to avoid

  1. Do not change the input parameters downloaded from the parent component in ngOnChange, ngOnInit, ngDoCheck
  2. Do not change the parent component or its own dom structure in ngAfterViewInit
  3. Changes can be made asynchronously
  4. Call this.cd.detectChanges(); at the end of the parent component’s ngAfterViewInit (not recommended)

Angular parent-child component life cycle

Reference link: https://zhuanlan.zhihu.com/p/93268483