3

Recently some errors occurred while using ngAfterViewInit. I also found that I don't really understand the process of change detection, so let's sort out the process and talk about why this error occurs.

change detection

First, let's take a look at angular's detection of components:

As Angular examines each component, these components perform the following actions in roughly the specified order :

  1. Update binding properties of all child components/directives (e.g. @Input)
  2. Call ngOnInit, OnChanges and ngDoCheck lifecycle hooks on all child components/directives
  3. Update the DOM of the current component
  4. Run change detection for child components
  5. Call ngAfterViewInit lifecycle hook for all child components/directives

image.png


But in development mode the following additional operational checks are performed:

After each action, Angular remembers the value of the variable it used to perform the action. They are stored in the oldValues property of the component view.

Angular does the following:

  • Check if the value passed to the child component is the same as oldValues
  • Check if the value used to update the DOM element is the same as oldValues
  • Perform the same check on all child components

Example of throwing ExpressionChangedAfterItHasBeenCheckedError error

for example:

Assuming it is now in development mode

The A component is defined and the text is passed to the B component

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

export class AComponent { 
    name = 'Im A'
    text = 'A to B`;
}
 @Component({ 
selector: 'b-comp',
 })
export class BComponent { 

 @Input() text; 

constructor(private parent: AComponent) {} 

ngOnInit() { 
    this.parent.text = 'B to A';
 }
}

Let's follow the 5 steps of change detection :

  1. Update binding properties of all child components/directives
    Angular first executes the binding between the text of the B component and the text of the A component, and the text of the B component = "A to B".
    In development mode, this value will be recorded view.oldValues[0] = 'pass to child components';
  2. Call hooks such as ngOnInit on the child component and execute AComponent.text = "B to A";

An exception occurs when the second step is executed , and ExpressionChangedAfterItHasBeenCheckedError is thrown
As shown below

image.png

This is the error thrown by violating the check in development mode.

In simple terms, the next step changes it the other way round while the previous step has established a good value. Conflicts with oldValue.

That is, the literal meaning of the word ExpressionChangedAfterItHasBeenCheckedError.


second example

If we do this in the subcomponent of B, will it throw an error?

 ngOnInit() { this.parent.name = 'updated name'; }

Maybe you think: Isn't this also changing the value of the parent component in ngOnInit, it will definitely report an error.

But the result is not, what is the reason?

Don't forget that name is defined in A like this:

 @Component({ 
template: ` 
 <span>{{name}}</span>
 })

export class AComponent { 
    name = 'Im A'
}

See something? That's right, the operation on the name is only executed in the third step.

That is: update the DOM of this component

image.png

Summarize:

In fact, the principle is very simple: after the value has been established in the previous step, do not change it in the next step, and do not conflict with oldValue

Examples in the project

The project encounters an example of error reporting in terms of dynamic components, and also throws ExpressionChangedAfterItHasBeenCheckedError.

Briefly describe the code:

 export class App {
    @ViewChild(FormItemDirective, {static: true})
    appFormItem: FormItemDirective;
 

    constructor(private r: ComponentFactoryResolver) {
    }

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

According to the 5-step process, the reason for the error is very simple:

This component dynamically adds a child component in ngAfterViewInit. Since adding a child component requires modifying the DOM, and after Angular updates the DOM, the ngAfterViewInit lifecycle hook is triggered to modify the DOM, thus throwing an error.

As shown in the figure: In the fifth step, go to modify the DOM that has been established in the third step.
image.png

Solution

Taking the modification in the project as an example, I will talk about how to solve the problem of modifying the DOM established in step 3 in step 5.

1. Bring the revision forward

A very simple method, since the DOM is established in the third step, it is not enough to modify it in the first and second steps.

  • Modified in the first step, @Input can be used because updating the bound property of the child component is done in the first step.

     @Input()
    set setValue(value: Type) {
        // do someting
    }
  • Modified in the second step, hooks such as ngOnInit are completed in the second step

     ngOnInit() {
     // do something
    }

2. Force Change Detection

Another possible solution is to force another change detection cycle for the parent A component. The best place is in the ngAfterViewInit lifecycle hook, as it is fired when change detection is performed on all child components, so they are more likely to update parent component properties.

This can be done using ChangeDetectorRef.detectChanges().

 export class AppComponent { 

constructor(private cd: ChangeDetectorRef) { } 

ngAfterViewInit() { 
    this.cd.detectChanges(); 
}

some questions

Why does angular need such validation?

Angular enforces so-called unidirectional data flow from top to bottom. After handling parent changes, components lower in the hierarchy are not allowed to update the parent component's properties.

This ensures that the entire component tree is stable after change detection. The tree is unstable if properties that need to be synchronized with consumers that depend on those properties change.

Why only run it in development mode?

Probably because the integration mode is not as severe as the development mode runtime error. After all it will probably stabilize in the next digest run.

However, it's better to just fix it in development mode rather than leave it to the client to try to debug it.


Reference article: https://hackernoon.com/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error-e3fd9ce7dbb4


weiweiyi
1k 声望123 粉丝