Angular Change Detection Strategies
All Angular apps are made up of a hierarchical tree of components. The state of a component can change anytime as a result of any kind of asynchronous operation, no matter if it’s coming from a user (events like a button click) or it’s a response coming back from a server.
When that happens Angular detects changes and immediately updates views. This is what we call a change detection. The main goal of the change detection mechanism is to make sure the underlying views are always in sync with their corresponding models.
Angular Change Detection
How Default Angular Change Detection Strategy Work?
As we already know, the building blocks of Angular applications are components that, unlike regular directives, always have a template. For every component at the application startup time, Angular creates a separate change detector. A change detector keeps track of a component's current and previous state.
During one change detection cycle, Angular checks all the change detectors starting from the root to its child nodes recursively to determine which ones have reported changes. The change detection cycle is always performed once for every detected change.
For each expression used in the template, the change detector is comparing the current value of the property used in the expression with the previous value of that property. If the old and the new property values are different, the property is marked as changed (isChanged = true).
When Angular gets the report from a change detector, it instructs the corresponding component to re-render and update the DOM accordingly. That's in principle implementation of the default change detection strategy (ChangeDetectionStrategy.Default).
Angular is pretty fast. But as an application grows, the number of changes increases as well. If every time a change happens, hundreds of expressions should be evaluated and many components in a sub-tree should be re-rendered, then Angular will have to work harder to keep track of all the changes. It sounds as if we might encounter a performance problem at some point. But Angular offers an alternative change detection strategy if we need to take over the control in this process. It’s called OnPush change detection strategy.
How to use OnPush Change Detection Strategy?
By setting a change detection strategy of our component to ChangeDetectionStrategy.OnPush (inside @Component decorator) we explicitly say that component only depends on its pure inputs and will be checked in the following cases:
- One of the Inputs of the component has changed
- An event handler of the component has been triggered
- Run a change detection explicitly.
When the template of a component does not only depend on the 1) and 2), but it also depends on some other asynchronous events, we are in charge of managing a change detection and re-rendering.
Let's create an example and see what the responsibilities and common pitfalls are when using this strategy.
The Input Reference Changes
Let’s create a component BookPreviewComponent that will display basic book information: a title, an author and a genre. These three pieces of information are properties of Book class, so the component has an input of type Book.
@Component({ selector: 'app-book-preview', templateUrl: './book-preview.component.html', styleUrls: ['./book-preview.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush})export class BookPreviewComponent implements OnInit, OnDestroy { @Input() book: Book; constructor() { } ngOnInit() { }}
<div *ngIf="book"> <p> {{book.title}} </p> <p> {{book.author}} </p> <p> {{book.genre}} </p></div>
In the ngOnInit() lifecycle hook of the app component, we define a book with an unknown genre. Additionally, the app component has a button with click event bound to updateGenre() that sets book genre value to 'Literary Fiction'.
@Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss']})export class AppComponent implements OnInit { public book: Book; ngOnInit() { this.book = new Book('The Old Man and the Sea', 'Ernest Hemingway', 'Unknown'); } public updateGenre() { this.book.genre = 'Literary Fiction'; }}
<div style="padding-left: 10%"> <app-book-preview [book]="book"></app-book-preview> <button (click)="updateGenre()">Update genre</button></div>
Let's try it. At first, we see the book 'The Old Man and the Sea' by Ernest Hemingway with the 'Unknown' genre. If we now click the button, the genre will remain ‘Unknown’, which means that something doesn't work correctly. Our change is not detected and the view is not in sync with the model anymore. If we remove the change detection strategy from BookPreviewComponent and try again, it works correctly.
We can explain this behavior with one simple rule: when using OnPush change detection we are obligated to work with immutable objects. The reason is simple - Angular doesn't do a deep comparison check; all it does is a simple reference check since it is way cheaper option.
In our case, we did not provide a reference to a new object. Instead we mutated an existing one, so the OnPush change detector did not get triggered. Ok, let's make a book immutable and make sure this works.
public updateGenre() { this.book = new Book('The Old Man and the Sea', 'Ernest Hemingway', 'Literary Fiction'); }
What Are Change Detection and Asynchronous Events?
OnPush change detection will be triggered every time an event handler of the component is triggered. That's something we’ve already learned and it can be proved easily.
Add a counter to the BookPreviewComponent and define a handler to increase the counter. Add a new button to the template and bind its click event to the increase function. Display the counter value in the template and test it. It works, right?
The template is in sync with the state of the component. But what happens when a counter value changes are triggered by some other source (service, API call, timer, etc.)?
Let’s create a service CounterService with a simple function increaseCounter() that will increase the counter and emit the updated value.
@Injectable()export class CounterService { private counterChangedSubject$: Subject<number> = new Subject<number>(); public counterChanged$: Observable<number> = this.counterChangedSubject$.asObservable(); private counter = 0; public increaseCounter() { this.counter++; this.counterChangedSubject$.next(this.counter); }}
If we call a service method from the app component on button click, and subscribe to value changes in the BookPreviewComponent, we will notice that the counter value changes are being received, but the template is not being refreshed. It refreshes only when something else triggers the change detection. Check the video below.
We would see the same behavior if we, instead of changing this value from service, use setTimeout(), setInterval() or subscribe to a HttpClient get call. It simply doesn’t detect any kind of asynchronous events except the ones that are coming from the component itself or its children. So, for example, when we are fetching some data from the API in the subscription body we will explicitly tell Angular to run a change detection once the data is received. Soon we will see how we can do that.
What is Async Pipe?
Async pipe subscribes to an Observable or Promise and returns the latest value it has emitted. In case you are wondering how that will fit with the OnPush change detection strategy (considering all the previously said about async events that are not detected) please pay attention to this sentence from Angular official documentation: „When a new value is emitted, the async pipe marks the component to be checked for changes. “ And if we take a look at the source code, we will see the following:
private _updateLatestValue(async: any, value: Object): void { if (async === this._obj) { this._latestValue = value; this._ref.markForCheck(); }}
Angular is calling markForCheck(), and that’s the reason why we don’t need to worry when we use async pipe combined with observables. Every time a new value is received it will be detected. So, when we work with observables we can always use async pipe, which will save us from triggering the change detection explicitly.
How to Trigger Change Detection Explicitly?
ChangeDetectorRef is a base class for Angular views and helps us manage a change detection. What we need is a way to refresh our BookPreviewComponent component every time the counter value changes. All we need to do is inject ChangeDetectionRef to our component and call detectChange() inside the subscription body. This will check the view and its children (if there are any) and refresh all the updated values.
@Component({ selector: 'app-book-preview', templateUrl: './book-preview.component.html', styleUrls: ['./book-preview.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush})export class BookPreviewComponent implements OnInit, OnDestroy { @Input() book: Book; public counter: number; private subscription = new Subscription(); constructor( private counterService: CounterService, private changeDetectorRef: ChangeDetectorRef) { } ngOnInit() { this.counter = 0; this.subscription.add( this.counterService.counterChanged$.subscribe((newValue: number) => { this.counter = newValue; this.changeDetectorRef.detectChanges(); console.log(`The counter value is ${this.counter}`); }) ); } ngOnDestroy() { this.subscription.unsubscribe(); }}
The second option provided as ChangeDetectorRef functionality is markForCheck() (we already saw it in the pipe implementation!). This will explicitly mark the view as changed so that it can be checked again. It does not trigger a change detection. However, by marking the view as changed we say to Angular that this view can be checked either as a part of the current or the next change detection cycle.
Additionally, there is ApplicationRef that can be injected into the component allowing change detection to be run for the whole application using tick() function.
What Did We Learn About Angular Change Detection Strategies?
For the majority of smaller applications, the default change detection strategy would be just fine. If you're working on a bigger application or tend to always keep your application optimized, OnPush change detection strategy can help you with that.
Keep in mind that when the default change detection strategy is in use, Angular doesn’t assume anything about your application. It just reacts to everything that is happening and refreshes all the components in the subtree. This often means we are running a change detection in many components for no reason.
OnPush change detection allows us to control when to re-render and do the checks. However, we should always keep in mind that everything, except pure inputs and events, is our responsibility when deciding on this approach.