The convention of Angular's data flow, in which data flows downward through the component tree and events float upwards, engenders some interesting possibilities. One of these involves controlling when Angular should perform change detection at a given node in the component tree.
The code, links, and a live example related to this recipe are available at .
Begin with the following simple application:
[app/root.component.ts] import {Component} from '@angular/core'; import {Subject} from 'rxjs/Subject'; import {Observable} from 'rxjs/Observable'; @Component({ selector: 'root', template: ` <button (click)="shareSubject.next($event)">Share!</button> <article [shares]="shareEmitter"></article> ` }) export class RootComponent { shareSubject:Subject<Event> = new Subject(); shareEmitter:Observable<Event> = this.shareSubject.asObservable(); } [app/article.component.ts] import {Component, Input, ngOnInit} from '@angular/core'; import {Observable} from 'rxjs/Observable'; @Component({ selector: 'article', template: ` <h1>{{title}}</h1> <p>Shares: {{count}}</p> ` }) export class ArticleComponent implements OnInit { @Input() shares:Observable<Event>; count:number = 0; title:string = 'Insurance Fraud Grows In Wake of Apple Pie Hubbub'; ngOnInit() { this.shares.subscribe((e:Event) => ++this.count); } }
This very simple application is just using an observable to pass share events down, from a parent component to a child component. The child component keeps track of the click event count and interpolates this count to the page.
Your objective is to modify this setup so that the child component only detects a change when an event is emitted from the observable.
Out of the box, Angular already has a very robust way of detecting change: zones. Whenever there is an event inside a zone, Angular recognizes that this event has the potential to modify the application in a meaningful way. It then performs change detection from the top of the component tree down to the bottom, checking whether anything needs to be updated in the changed model. Since data only flows downward, this is already an extremely efficient way of handling it.
However, you might like to exert some control in this situation since you may be able to hand-optimize when change detection should occur inside a component. Most likely you will very easily be able to tell when a component may or may not change, based on what is happening around it.
Angular offers you the option of changing how the change detection scheme works. If you would prefer that Angular refrain from listening to zone events to kick off a round of change detection, you can instead configure a component to only perform change detection when an input is changed. This can be done by configuring the component to use the OnPush
strategy instead of the default.
Surely, this will occur less often than the firehose of browser events. If the component will only change when an input is changed, then this will save Angular the trouble of doing an iteration of change detection on that component.
Modify ArticleComponent
to instead use OnPush
:
[app/article.component.ts] import {Component, Input, ngOnInit, ChangeDetectionStrategy} from '@angular/core'; import {Observable} from 'rxjs/Observable'; @Component({ selector: 'article', template: ` <h1>{{title}}</h1> <p>Shares: {{count}}</p> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class ArticleComponent implements OnInit { @Input() shares:Observable<Event>; count:number = 0; title:string = 'Insurance Fraud Grows In Wake of Apple Pie Hubbub'; ngOnInit() { this.shares.subscribe((e:Event) => ++this.count); } }
This successfully changes the strategy, but there is one significant problem now: the count will no longer be updated. This happens, of course, because the input to this component is not being changed.
The count only updated before because Angular was seeing click events on the button, which caused change detection on ArticleComponent
. Now, Angular has been instructed to ignore these click events, even though the count inside the child component is still being updated from the observable handlers.
You are able to inject a reference to the component's change detector. With this, it exposes a method that allows you to force a round of change detection whenever you like. Since the model is being updated inside an observable handler, this seems like a fine place to trigger the change detection process:
[app/article.component.ts] import {Component, Input, ngOnInit, ChangeDetectionStrategy, ChangeDetectorRef} from '@angular/core'; import {Observable} from 'rxjs/Observable'; @Component({ selector: 'article', template: ` <h1>{{title}}</h1> <p>Shares: {{count}}</p> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class ArticleComponent implements OnInit { @Input() shares:Observable<Event>; count:number = 0; title:string = 'Insurance Fraud Grows In Wake of Apple Pie Hubbub'; constructor(private changeDetectorRef_: ChangeDetectorRef) {} ngOnInit() { this.shares.subscribe((e:Event) => { ++this.count; this.changeDetectorRef_.markForCheck(); }); } }
Now you will see the count getting updated once again.
Using OnPush
essentially converts a component to operate like a black box. If the input doesn't change, then Angular assumes that the state inside the component will remain constant, and therefore there is no need to proceed further with regard to detecting a change.
Since change detection always flows from top to bottom in the component tree, the request for change detection uses markForCheck()
, marks the component's change detector to run only when Angular reaches that component inside the tree.
This is a useful pattern if you're looking to squeeze additional performance from your application, but in some cases, doing this can develop into an anti-pattern. The need to explicitly define when Angular should perform change detection can become tedious as your component code grows in size. There can potentially be bugs that arise from missing one place where markForCheck()
should have been invoked but was not. Angular's change detection strategy is already quite performant and robust, so use this configuration wisely.