📜 ⬆️ ⬇️

Angular onPush Complete Change Detection Strategy Guide

image


Default change detection strategy


By default, Angular uses the ChangeDetectionStrategy.Default change detection strategy.


ChangeDetectionStrategy.Default works in such a way that every time something changes in our application, as a result of various user events, timers, XHR, promises, etc., change detection will run on all components.


We can see this with a simple example. Create a getter component and use it in our template. For example:


@Component({ selector: 'hello', template: ` <h1>Hello {{name}}!</h1> {{runChangeDetection}} ` }) export class HelloComponent { @Input() name: string; get runChangeDetection() { console.log('Checking the view'); return true; } } 

 @Component({ selector: 'app-root', template: ` <hello></hello> <button (click)="onClick()">Trigger change detection</button> ` }) export class AppComponent { onClick() {} } 

Every time we press a button, Angular will launch a change detection cycle and in the console we will see two “Checking the view” logs (or one in the production mode).


This cycle is called dirty checking. The essence of the test is that Angular compares the new values ​​with the old ones and updates the view if they are not equal.


Now imagine a large application with a huge number of components and a variety of conditions. If we allow Angular, each time the change detection cycle starts, to check each of these conditions, it will negatively affect performance.


Despite the fact that Angular is well optimized, as the application grows, it will have to work more and more.


What if we help Angular and give it more explicit indicators when to check our components?


OnPush change detection strategy


We can change ChangeDetectionStrategy.Default to ChangeDetectionStrategy.OnPush .


This means that the component now depends only on @inputs () parameters, and will be checked only under the following conditions:


1. The parameter input reference has changed.


Having established the OnPush strategy, we kind of sign a contract with Angular, which obliges us to work with immutable objects (or observables, as we will see later).


The advantage of working with immutable data in the context of detecting changes is that Angular performs a simple reference check to decide whether to check the presentation. This is much faster than a deep comparison of objects.


Let's try to mutate an object and look at the result:


 @Component({ selector: 'tooltip', template: ` <h1>{{config.position}}</h1> {{runChangeDetection}} `, changeDetection: ChangeDetectionStrategy.OnPush }) export class TooltipComponent { @Input() config; get runChangeDetection() { console.log('Checking the view'); return true; } } 

 @Component({ selector: 'app-root', template: ` <tooltip [config]="config"></tooltip> <button (click)="onClick()">Click</button> ` }) export class AppComponent { config = { position: 'top' }; onClick() { this.config.position = 'bottom'; } } 

After clicking on the button, we will not see any logs in the console. Angular simply compared the old and new values ​​by reference, which remained the same.


 /** Returns false in our case */ if( oldValue !== newValue ) { runChangeDetection(); } 

As we know, numbers, booleans, strings, null and undefined are primitive types. All primitive types are passed by value. Objects, arrays, and functions are passed by reference.


To start the change detection mechanism, we need to change the object reference itself.


 @Component({ selector: 'app-root', template: ` <tooltip [config]="config"></tooltip> <button (click)="onClick()">Click</button> ` }) export class AppComponent { config = { position: 'top' }; onClick() { this.config = { position: 'bottom' } } } 

After this change, we will see that the view has been verified and the value has changed, as we expected.


2. The event inside the component or its descendants


A component can have an internal state that is updated when an event from the component itself or its descendants occurs.


For example click:


 @Component({ template: ` <button (click)="add()">Add</button> {{count}} `, changeDetection: ChangeDetectionStrategy.OnPush }) export class CounterComponent { count = 0; add() { this.count++; } } 

When we click on the button, Angular starts the change detection cycle and the view is updated as expected.


Perhaps you think that all asynchronous operations should also be a trigger for starting the change detection mechanism, as we said at the beginning, but no. The rule applies only to DOM events, so the following code will not trigger the mechanism.


 @Component({ template: `...`, changeDetection: ChangeDetectionStrategy.OnPush }) export class CounterComponent { count = 0; constructor() { setTimeout(() => this.count = 5, 0); setInterval(() => this.count = 5, 100); Promise.resolve().then(() => this.count = 5); this.http.get('https://count.com').subscribe(res => { this.count = res; }); } add() { this.count++; } } 

Notice that the count property has changed, so the next cycle for detecting changes, when we click the button, the value will be 6 (5 + 1).


3. Manual start of change detection


Angular provides us with three methods for launching the change detection mechanism ourselves and we can call them in the places we need.


The first detectChanges () tells Angular to start detecting changes in a component and its descendants.


 @Component({ selector: 'counter', template: `{{count}}`, changeDetection: ChangeDetectionStrategy.OnPush }) export class CounterComponent { count = 0; constructor(private cdr: ChangeDetectorRef) { setTimeout(() => { this.count = 5; this.cdr.detectChanges(); }, 1000); } } 

The second ApplicationRef.tick () tells Angular to start detecting changes in the entire application.


 tick() { try { this._views.forEach((view) => view.detectChanges()); ... } catch (e) { ... } } 

And the third markForCheck () , which does not trigger the start of change detection. Instead, it marks the component and all its parents that they should be checked in the current or next change detection cycle.


 markForCheck(): void { markParentViewsForCheck(this._view); } export function markParentViewsForCheck(view: ViewData) { let currView: ViewData|null = view; while (currView) { if (currView.def.flags & ViewFlags.OnPush) { currView.state |= ViewState.ChecksEnabled; } currView = currView.viewContainerParent || currView.parent; } } 

It is worth noting that manual starting the change detection mechanism is not a bad practice. The use of these methods is quite by design and permissible (of course, if necessary).


Angular Async Pipe


Async pipe subscribes to the observed object or promise and returns the last value given to it.


Let's look at a simple example of a component with OnPush and an observable input parameter.


 @Component({ selector: 'app-root', template: ` <button (click)="add()">Add</button> <app-list [items]="items$"></app-list> ` }) export class AppComponent { items = []; items$ = new BehaviorSubject(this.items); add() { this.items.push({ title: Math.random() }) this.items$.next(this.items); } } 

 @Component({ selector: 'app-list', template: ` <div *ngFor="let item of _items">{{item.title}}</div> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class ListComponent implements OnInit { @Input() items: Observable<Item[]>; _items: Item[]; ngOnInit() { this.items.subscribe(items => { this._items = items; }); } } 

After clicking on the button, we will not see the updated view. This is due to the fact that none of the conditions described above has happened. Therefore, Angular will not check the component in the current change detection cycle.


Now let's use async pipe.


 @Component({ template: ` <div *ngFor="let item of items | async">{{item.title}}</div> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class ListComponent implements OnInit { @Input() items; } 

As you can see, the view is now updated. When a new value arrives, the async pipe marks the component as needed for the check. We can see it in the source :


 private _updateLatestValue(async: any, value: Object): void { if (async === this._obj) { this._latestValue = value; this._ref.markForCheck(); } } 

Angular calls markForCheck () , so the view is updated even when the links have not changed.


If a component depends only on incoming parameters, and they are observable, then this component can change only if one of the parameters generates an event.


Tip: Antipatter is considered to be given to the outside world by subject, it is better to always give the observed object using the asObservable () method.


OnPush and template variables


Consider the following components:


 @Component({ selector: 'app-tabs', template: `<ng-content></ng-content>` }) export class TabsComponent implements OnInit { @ContentChild(TabComponent) tab: TabComponent; ngAfterContentInit() { setTimeout(() => { this.tab.content = 'Content'; }, 3000); } } 

 @Component({ selector: 'app-tab', template: `{{content}}`, changeDetection: ChangeDetectionStrategy.OnPush }) export class TabComponent { @Input() content; } 

 <app-tabs> <app-tab></app-tab> </app-tabs> 

You probably think that Angular will update the app-tab view of the component with a new value in 3 seconds.


We have seen that if we update the link in the OnPush component, this should trigger a change detection mechanism, shouldn’t it?


Unfortunately, in this example it does not work. Angular does not know that we have updated the property in the app-tab component. Only by defining the input parameter in the template, Angular will understand that this property should be checked in the next change detection cycle.


For example:


 <app-tabs> <app-tab [content]="content"></app-tab> </app-tabs> 

Since we have explicitly defined the input parameter in the template, Angular will create the updateRenderer () function, which tracks the changes in the parameter values ​​in each change detection cycle.


image
AppComponent.ngfactory.ts


A simple solution in this case is to create a setter and call markForCheck () .


 @Component({ selector: 'app-tab', template: `{{_content}}`, changeDetection: ChangeDetectionStrategy.OnPush }) export class TabComponent { _content; @Input() set content(value) { this._content = value; this.cdr.markForCheck(); } constructor(private cdr: ChangeDetectorRef) {} } 

=== onPush ++


Once we understand (hopefully) the full power of OnPush, we can create more optimized applications. The more components with the OnPush strategy we have, the fewer checks Angular will perform. Consider a real example:


Suppose we have a todos component in which the todos property is defined with the Input decorator.


 @Component({ selector: 'app-todos', template: ` <div *ngFor="let todo of todos"> {{todo.title}} - {{runChangeDetection}} </div> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class TodosComponent { @Input() todos; get runChangeDetection() { console.log('TodosComponent - Checking the view'); return true; } } 

 @Component({ template: ` <button (click)="add()">Add</button> <app-todos [todos]="todos"></app-todos> ` }) export class AppComponent { todos = [{ title: 'One' }, { title: 'Two' }]; add() { this.todos = [...this.todos, { title: 'Three' }]; } } 

The disadvantage of this example is that when you click the Add button, Angular will check each todo for changes. And at the first click we will see three logs in the console.


In the example above, only one check is done, but imagine a component from a real large application, with several conditions and data bindings (ngIf, ngClass, expressions, etc.). This can significantly affect performance.


We run change detection for no reason.


An effective solution is to create a component with OnPush strategy for a specific todo. For example:


 @Component({ selector: 'app-todos', template: ` <app-todo [todo]="todo" *ngFor="let todo of todos"></app-todo> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class TodosComponent { @Input() todos; } 

 @Component({ selector: 'app-todo', template: `{{todo.title}} {{runChangeDetection}}`, changeDetection: ChangeDetectionStrategy.OnPush }) export class TodoComponent { @Input() todo; get runChangeDetection() { console.log('TodoComponent - Checking the view'); return true; } } 

Now when you click on the button, we will see only one log. Since none of the input parameters of the other todo has changed, their presentation will not be checked.


Also, the logical separation of components will make your code more readable and reusable.


')

Source: https://habr.com/ru/post/358860/


All Articles