This article has been translated with the consent of the original author.link

The body of the

In stackoverflow recently always see someone asked when using presents ExpressionChangedAfterItHasBeenCheckedError error. Most of the people asking questions don’t understand Angular’s change-monitoring mechanism or why the error message is needed. Some developers even think it’s a bug, but it’s not a bug.

This article will delve into the causes of this error and the principles detected, show the common scenarios in which this error occurs, and suggest several possible solutions. The final chapter explains why Angular needs this monitoring mechanism.

About Change Monitoring

Every Angular application is presented as a component tree. Angular performs operations on each component in the change monitoring phase (List1) in the following order:

  • Update all properties bound to child Component/Directive
  • To call all children of Component/DirectivengOnInit , ngOnChanges , ngDoCheckLife cycle function
  • Parse and update the value in the DOM of the current component
  • Run the change monitoring process for the child Component (List1)
  • Calls all of the children of Component/DirectivengAfterViewInitThe life cycle

There are other operations that are performed during the change detection phase, and I detail these processes in this article: Everything You need to know about change detection in Angular

After each action, Angular saves the values associated with that action in the component view’s oldValues property. (in development mode) After all components have completed change monitoring Angular starts the next monitoring process. The second monitoring process does not re-execute the change monitoring process listed above, but compares the values saved in the previous change monitoring cycle (in oldValues) to the current monitoring process (List2) :

  • Check that values (oldValues) passed to the child component are the same as values (instance.value) that the current component is to be used for updates
  • Check that the values (oldValues) used to update DOM elements are the same as the values (instance.value) currently being used to update these components
  • Perform the same check on all child Components

Note: These additional checks (List2) only occur in development mode, and I’ll explain why in a later section.

Let’s look at an example. Suppose you have A parent component A and A child component B. Component A has two attributes: name and text. Component A uses the name attribute in its template:

template: '<span>{{name}}</span>'
Copy the code

Then add component B to the template and bind it to component B with input property text property:

@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`;
Copy the code

So what happens when Angular starts change monitoring? (List1) Change monitoring starts with component A, passing A message for the Child component from the text expression down to component B and storing the value on the view:

view.oldValues[0] = 'A message for the child component';
Copy the code

Then the second step in the change monitor list calls the corresponding lifecycle function.

Next, perform the third step, parsing the {{name}} expression into I am A component text. Update the parsed value to the DOM and store it in oldValues:

view.oldValues[1] = 'I am A component';
Copy the code

Angular finally performs the same action on component B (List1). Once component B completes the above action, the change monitoring cycle is complete.

If Angular is running in development mode, another monitoring process (List2) is executed. The text property is passed to component B with the value A message for the Child Component stored in oldValues. Now imagine component A updates the value of text to updated Text after that. Then the first step of List2 will check if the text property has been changed:

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

Angular should throw this error at this point

ExpressionChangedAfterItHasBeenCheckedError.

Similarly, the same error is thrown if the update is already rendered in the DOM and stored in the name attribute in oldValues

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

Now you might be wondering, how can these values be changed? Let’s move on.

Reasons for data changes

The culprit is usually a subcomponent or directive, but let’s look at a simple example. I will try to recreate the scene with as simple an example as possible, and I will give real-life examples later. We all know that A parent component can use A child component or directive. Here we give A parent component A, child component B, and component B has A binding property text. We’ll update the text property in the child component’s ngOnInit (at this point the data is bound) lifecycle hook:

export class BComponent { @Input() text; constructor(private parent: AppComponent) {} ngOnInit() { this.parent.text = 'updated text'; }}Copy the code

We see the error of expectation:

Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'A message for the child component'. Current value: 'updated text'.
Copy the code

Now we do the same for the name property used for the parent component template:

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

There is no error in the program. Why is that?

If you look closely at the order in which change monitoring (List1) is executed, you’ll see that the ngOnInit of the child component will be called before the current Component’s DOM is updated (changing the data before logging oldValues), which is why changing the name property in the above example is not an error. We need a hook with updated values in the DOM to experiment, and ngAfterViewInit is a good choice:

export class BComponent { @Input() text; constructor(private parent: AppComponent) {} ngAfterViewInit() { this.parent.name = 'updated name'; }}Copy the code

Once again, we get the expected error:

AppComponent.ngfactory.js:8 ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'I am A component'. Current value: 'updated name'.
Copy the code

In reality, of course, the situation is more complex, and the updating of properties in the parent component before secondary monitoring is usually indirectly caused by the use of external services or observabals. But the underlying reasons are the same.

Now let’s look at some real life cases.

Shared services

Example: Plunker. In this application, the parent component and the child component share a shared service, and the child element sets the value of an attribute through the shared service and reflects it on the parent element. In this pattern, the subelement changes the parent element’s value in a way that is not as obvious as in the simple example above, but indirectly updates the parent element’s attributes.

Synchronous event broadcast

Example: Plunker. In this application, the parent element listens for an event broadcast by a child element that causes the parent element’s property to be updated, which in turn is used in the child element’s Input binding. This also indirectly updates the attributes of the parent element.

Dynamic component instantiation

This pattern is slightly different from the previous two patterns, where the first step in List2 detects errors thrown by DOM update detection (List2 step 2). Example: Plunker. The parent component in this application dynamically adds a child component to the ngAfterViewInit lifecycle after the initial DOM update of the current component. Adding a child component modifies the DOM structure. The VALUES used in the DOM are different (if the child component has a new value reference), so an error is thrown.

Possible solutions

If you look carefully at the last sentence of the wrong message:

Expression has changed after it was checked. Previous value:… Has it been created in a change detection hook ?

In the case of dynamically created components, the best solution to this problem is to change the lifecycle hooks in which the components are created. For example, the process of dynamically creating components in the previous section can be moved to ngOnInit. Even though the documentation states that ViewChildren can only be retrieved after ngAfterViewInit, the child components are being populated when the view is created, so ViewChildren can be retrieved ahead of time.

If you’ve googled this error, you’ve probably seen some of the answers that recommend both asynchronously updating the data and forcing a change monitoring loop to fix it. Even though I have listed these two methods, I would recommend redesigning your application rather than using these two methods to solve this problem, and I will explain why in a later article.

Asynchronous update

One thing you should notice is that both change monitoring and the second validation digest are performed synchronously. This means that if we update the values of the properties asynchronously in our code, the properties will not be changed when the second validation loop is run, and no errors will be reported. Let’s try it:

export class BComponent { name = 'I am B component'; @Input() text; constructor(private parent: AppComponent) {} ngOnInit() { setTimeout(() => { this.parent.text = 'updated text'; }); } ngAfterViewInit() { setTimeout(() => { this.parent.name = 'updated name'; }); }}Copy the code

With no errors thrown, setTimeout adds the function to the MacroTask queue, which will be called in the next VM cycle. You can also add a function to the current VM cycle by using the then callback in promise after the other synchronized code has been executed:

Promise.resolve(null).then(() => this.parent.name = 'updated name');
Copy the code

Promise.then is not put into macroTask, but creates a microTask. The MicroTask queue is executed after all synchronized code has been executed in the current cycle, so updates to properties occur after the validation step. To learn more about micro and Macro tasks in Angular, see I Reverse-engineered Zones (zone.js) and here is What I’ve Found.

Passing True to EventEmitter will make the emit of the event asynchronous:

new EventEmitter(true);
Copy the code

Forced change monitoring

Another solution is to force A change-monitoring loop between the first and second validation of the parent component A. The best place to trigger forced change monitoring is during the ngAfterViewInit lifecycle, when all child components’ processes have finished executing, so it doesn’t matter if you change the parent component’s properties at any previous point:

export class AppComponent {
    name = 'I am A component';
    text = 'A message for the child component';

    constructor(private cd: ChangeDetectorRef) {
    }

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

Well, there are no errors, and I can run the program happily. The problem is that when A newly added change monitor is triggered in the parent component A, Anuglar also runs change monitor once for all the child components, so the parent component may be updated again.

Why is a second monitoring cycle needed

Angular enforces top-down, one-way data flow, and does not allow the inner child to change the parent’s properties until the second time after the parent completes change monitoring. This ensures that the component tree is stable after the first change monitoring. The component tree is unstable if properties change during the monitoring cycle so that consumers who depend on those properties need to update the changes synchronously. In the example above, the sub component B depends on the parent component’s text property, and whenever the value of the property changes, the component tree is in an unstable state until those changes are passed to component B. This is also reflected in the relationship between the DOM and properties, as the DOM acts as a consumer of those properties and then renders them to the UI. If some properties are not synchronized to the interface, the user will see the wrong interface.

The synchronization of the data stream takes place in the two heap operations listed at the beginning of this article, so what happens if you modify the properties in the parent component through the child component after the synchronization is complete? Yes, you are left with an unstable tree of components in which the order of data changes will be unpredictable. Most of the time this will give users a page with incorrect data, and troubleshooting will be very difficult.

You may ask why not wait until the component tree is stable before monitoring for changes? The simple answer is that the component tree may never be stable. A child updates a property in the parent, which in turn updates the state of the child, which in turn triggers an update of the properties of the parent… It’s going to be an infinite loop. I’ve shown a lot of cases where components update or rely on attributes directly, but in the real world applications update and rely on attributes indirectly and not easily.

Interestingly, AngularJS(Angular 1.x) does not use one-way data flow to keep the component tree stable to a large extent. But more often than not, we see an infamous error 10 $digest() iterations reached. Aborting! . A quick Google search will turn up plenty of questions about this error.

The final question is, why does the second loop monitor only run in development mode? I suspect that this is because data layer instability does not cause noticeable errors while the framework is running, since the data will be stabilized after the next monitoring cycle. Of course, it’s better to have bugs fixed during development than to have bugs fixed in a live application.

The first translation is slightly awkward, if there are mistakes welcome to point out.