In Angular 1.x, an ancestor of the newest Angular, there was a $digest cycle. It was a tool for listening to the component’s changes. Unfortunately, it caused a lot of issues with the optimization of applications. In the new Angular, creators replaced it with Change Detection.
Change Detection is a build-in mechanism that is responsible for checking bindings in the component’s template. It turns on in several cases, after:
- API calls
- DOM events, e.g. after clicking on the button, submitting the form
- usage of setTimeout(), setInterval()
@Component({
selector: 'counter',
template: '<span (click)=”incrementCounter()”>{{ counter }}</span>'
})
export class CounterComponent {
counter = 0;
incrementCounter() {
this.counter += 1;
}
}
In the code above, there is a simple binding of the counter variable. After clicking on the span element, the Change Detector will be turned on and will start the notification process.
Each component has its Change Detector instance that is created automatically in the application runtime phase. When some Change Detector meets the change in bindings, it turns on all Change Detectors for the whole components tree. As you may know, components in Angular have a tree structure. It means that when you are implementing components in Angular, you put them into each other, and you create a tree of components with AppComponent as a root.
In the presented graphic, you can see one branch of the Angular component’s tree. Let’s imagine the situation when some change in bindings is detected in UserCounterComponent:
- Change Detector in UserCounterComponent detects change
- Change Detector related to UserCounterComponents uses ngZone for notifying AppComponent Change Detector that it detected change
- AppComponent Change Detector receives information from UserCounterComponent Change Detector. It doesn’t know what changed in UserCounterComponent, but it has to notify other Change Detectors in the branch
- AppComponent Change Detector notifies other components Change Detectors from the up to the down: AppComponent CD -> UserProfile CD -> UserVisits CD - UserCounter CD
- Notified Change Detectors force re-rendering
Change Detection OnPush strategy
@Component({
selector: 'counter',
template: '<span>{{ counter }}</span>',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
@Input() counter = 0;
In @Component decorator, we can change the Change Detection strategy. The default value is automatically set to ChangeDetectionStrategy.default. We can change it to the OnPush strategy. OnPush strategy allows us to optimize our app a bit. It informs Change Detector that our component depends only on its @Input() and Change Detection mechanism for this component will be turned on only when the value in @Input() changes or when we force detection manually.
@Component({
selector: 'counter',
template: '<span (click)=”incrementCounter()”>{{ counter }}</span>',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
@Input() counter = 0;
incrementCounter() {
this.counter += 1;
}
}
It means that after clicking on span element, the Change Detector will not be turned on. The component that depends only on @Input() will be re-render when the @Input() changes, so other events from this component will not notify Change Detector from AppComponent and the application will be re-rendered less and in cases when it will be necessary. Change Detection in component with OnPush strategy will be also turned on when some Observable in this component will get new data from the stream.
ExpressionChangedAfterItHasBeenCheckedError
While working with Angular, you probably had this error, not only once. What does it mean?
@Component({
selector: 'timer',
template: `
<span [textContent]="time | date:'hh:mm:ss:SSS'"></span>
<button (click)="0">Trigger Change Detection</button>
`
})
export class TimerComponent {
get time() {
return Date.now();
}
}
Let’s take a look at the example above. We have a span element that displays the current time formatted by date pipe and a simple button with an empty callback function. When the user clicks on the button, the Change Detection mechanism turns on and checks bindings. It notifies the AppComponent Change Detector, and after the whole notification flow, the TimerComponent is re-rendered. Everything is working properly, but in the console, we have the following error:
Why do we have this error?
Well, after each change detection cycle, Angular runs the detection cycle one more time to ensure that all bindings have the same value as in the previous cycle. The check cycle is launched just after the end of the previous cycle. It detects differences and throws the error.
Although, why does Angular need this check? Well, imagine that some properties of components have been updated during the change detection cycle. As a result, expressions produce new values that are inconsistent with the previous ones. Angular could run another change detection cycle to synchronize the application state with the user interface. What if, during that process, some properties are updated again? Angular could fall in an infinite loop of change detection runs. That happened quite often in Angular 1.x.
ExpressionChangedAfterItHasBeenCheckedError is thrown to avoid this situation Angular detection cycle.
You can work around issues related to ExpressionChangedAfterItHasBeenCheckedError by injecting ChangeDetectorRef in your component and run method detectChanges from ChangeDetectorRef in ngAfterViewChecked life cycle hook. It will force the detection cycle once again and update bindings.
Angular is a huge and powerful framework. Knowledge about one of the most important mechanisms in Angular could be useful in current work. Information about Change Detection may also be useful with avoiding ExpressionChangedAfterItHasBeenCheckedError.