Change Detection (Part 2)
Implementing change detection strategies in Angular can sometimes be a frustrating endeavor as the internet offers many solutions which often don't take the time to explain what the author is trying to accomplish. Most often, you will just see blanket statements by people to always use the "onPush" change detection strategy.
While I agree completely with statements online that using an "onPush" change detection strategy will improve the performance of your Angular application, I'd like to also state that you can create advanced MPages that have no visible performance impact by sticking with default Angular change detection.
As mentioned in part 1 of our change detection training, the biggest problem you will encounter is if you are repeatedly making expensive function calls. The peopleByGender() method in our PeopleService class is an example of an expensive call as it has to perform the filter operation on every change detection cycle. For a small subset of data you won't notice any performance issues, however as your volume of data increases performance may become so poor that your application stops responding completely.
info Dealing with Frozen Applications
Sometimes your Angular application may appear frozen. Controls might be unresponsive and your screen may not be updating information. During the past number of years developing MPages, two reasons are most often the culprit for application freezing.
-
Performing operations on undefined variables and objects can cause freezing or prevent any code running
after the call to the undefined object to execute. This problem can be remedied by either using strict
type declarations with custom interfaces or if you are in a position where you are using data that can't
always be classed into a structure, performing a check before usage is a simple solution.
if (myPossiblyUndefinedObject) { ... your code using myPossiblyUndefinedObject ... }
-
Resource intensive operations running on every change detection cycle will make your application appear
frozen to the user. Our peopleByGender() method is a great example of something that can be resource
intensive. Some solutions to this problem are:
- Perform the operation once and store it in a variable or object. Check to see if the object has been populated and if it has return the new variable/object. If it hasn't, perform the expensive operation storing the results in memory as your variable/object.
- Use the "onPush" change detection strategy in Angular to prevent the component from running the normal change detection.
onPush Change Detection
-
To demonstrate the onPush change detection strategy we are going to start by putting our counter back into
our display. Open home.component.html and uncomment the counter.
If you view your page in the browser, you should see your counter rapidly increasing.Counter = {{ counter }}
-
Open home.component.ts and add the import of "ChangeDetectionStrategy" from
'@angular/core'. Your first import statement should appear as follows.
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
-
In the @Component decorator add you change detection strategy.
@Component({ selector: 'app-home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush })
- View your application in your web browser. The counter should now stay at the value of zero as change detection is no longer being performed on your component. It should be noted that change detection is still happening on the app.component component, but your child home.component has automatic change detection turned off.
- While viewing your page, either click on the line "The secret is Not for you." or modify the text in the "Enter a new Secret" input field. You should see your counter increment by one each time you perform and action (the first action increments by two but all subsequent actions should be by one). This happens because Angular will automatically force a change detection push when an action such as a button click or input control is triggered or input if the value of a primitive data type (e.g. string, Date, number, boolean) is changed. Changes to arrays or objects will not trigger change detection without intervention by you.
Manually forcing change detection
At this time, your template counter method is only run when an action is taken by the user. For many components this is absolutely fine and will perform very well. There are however times when the state of your data changes externally to user input. A good example is loading data from CCL inside your component. Your application needs to be instructed when to perform change detection after your data has been updated.
This can be accomplished by importing the ChangeDetectorRef class from @angular/core and calling the detectChanges() method when needed. Since we are not using external data in our lesson, we will instead set up a timer that runs every second to trigger the change detection so our counter can increase.
-
Add ChangeDetectorRef to your @angular/core import statement in home.component.ts.
import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
-
Add a new private variable in called timer to your class. This variable will be a NodeJS.Timer type.
private timer: NodeJS.Timer;
-
Expose the ChangeDetectorRef class in your constructor as an object called cdr.
constructor(public peopleService: PeopleService, public cdr: ChangeDetectorRef) {
-
We need to have a place that our timer object is guaranteed to be run. The constructor method is called when
your component is first assigned in your application. Add the following code to your constructor method:
constructor(public peopleService: PeopleService, public cdr: ChangeDetectorRef) { // Trigger change detection every second this.timer = setInterval(() => { cdr.detectChanges(); }, 1000); }
The setInterval method will run the same block of code repeatedly for the assigned duration. In this example we are running code inside an arrow function that repeats every 1,000 milliseconds (1 second). - View your application in your browser. You should now see the counter incrementing by one every second. This happens when the detectChanges() method is executed as it instructs Angular to examine the current component for any changes which means any methods (e.g. counter()) are called.
-
To demonstrate this functionality further, we are going to add some code to add one year to the age of all
the females in our people object inside PeopleService. We are going to make use of our peopleByGender()
method, the forEach method of an array and an arrow function. Modify your constructor so it appears as follows:
constructor(public peopleService: PeopleService, public cdr: ChangeDetectorRef) { // Trigger change detection every second this.timer = setInterval(() => { // Increment age of all females by one year. this.peopleService.peopleByGender('Female').forEach((female: IPerson) => { female.age += 1; }); cdr.detectChanges(); }, 1000); }
In the code changes above, on every interval of the timer we are first using the peopleByGender method to return an array of IPerson objects. Using JavaScript method chaining, we then use the forEach method on our array of Females, passing each IPerson object to our arrow function. Once inside the arrow function we simply add one to the age of the female record. Objects are passed by reference which means any change that happens inside functionality such as this happens to the originating object. - View your application in the browser. You should see all the females get older by a year every second where the age of the males remain the same.
Updating Input Values
Earlier I mentioned that change detection occurs on Input() values if they are one of the primitive data types such as a string, number, date or boolean. Objects such as our IPerson or any do not have any type of automatic detection when using onPush.
We will prove this in with the following exercise.
-
Add "Input" to your imports from '@angular/core' in home.component.ts.
import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef, Input } from '@angular/core';
-
Add two new @Input properties called stringInput and personInput to the HomeComponent class definition in
home.component.ts. The stringInput property should be a string type and the personInput
property should be an IPerson type.
@Input() stringInput!: string; @Input() personInput!: IPerson;
-
Comment out the cdr.detectChanges() line in your home.component.ts file. This will turn
off the change detection we have running every second.
// cdr.detectChanges();
-
Add the following output of our two new Input() properties to the top of the source code in
home.component.html.
<p>stringInput = {{ stringInput }}</p> <p>personInput = {{ personInput | json }}</p>
Viewing your page at this time should have no errors, however your values for stringInput and personInput will be blank on screen. -
Open app.component.ts and import IPerson as well as a string called stringValue
and an IPerson object called personValue. Your entire app.component.ts should appear
as follows where complete.
import { Component } from '@angular/core'; import { IPerson } from './people.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent { public stringValue = 'Hello'; public personValue: IPerson = {nameFirst: 'John', nameLast: 'Simpson', age: 51, gender: 'Male'} }
-
Open app.component.html and modify the source to match the content shown below. With
these changes we are creating HTML input elements to allow modification of stringValue and the nameLast
property of our personValue object. In addition to these changes, the stringInput property on our
<app-home></app-home> component is being set to stringValue and personValue is being assigned
to personInput.
<h2>App Level Inputs</h2> <p> stringValue = <input type="text" [(ngModel)]="stringValue"> </p> <p> personValue nameLast = <input type="text" [(ngModel)]="personValue.nameLast"> </p> <h2>Home Level Output</h2> <app-home [stringInput]="stringValue" [personInput]="personValue"></app-home>
- View your application in the browser. You should see inputs for stringValue and personValue nameLast that you can type new values into. Directly below the inputs you will see the values being displayed from within the <app-home> component.
-
Change the values in both fields starting with stringValue, and you should see stringValue being updated in app-home but personInput remains unchanged. As you can see, your primitive data type updates immediately as you type where your object does not update inside the component at all. If you go back and change the value of stringValue again you will see the nameLast value finally update as the change to stringValue forces change detection.
The problem we are witnessing is due to how Angular detects changes on objects. If we were assigning an entirely new object to personValue and not simply modifying one value at a time, change detection would occur as expected. In our example however, we are passing an entire object to our component that has no way of knowing that values have changed inside the object.
Personally I tend to avoid situations where I need to send an entire object to a component that gets updated in another adjacent component however there are ways to make our code work which we will demonstrate.
ViewChild
Angular offers functionality to expose the public methods and variables of our child components to a parent component. This is done with ViewChild or ViewChildren. As expected from the naming, ViewChild allows access to a single child component where ViewChildren allow access to multiple components of the same type.
-
Open app.component.html and modify our personValue input line to call a new method
called updateChildren() on a model change.
personValue nameLast = <input type="text" [(ngModel)]="personValue.nameLast" (ngModelChange)="updateChildren()">
Once we implement it, as new characters are typed into the input field, our updateChildren() function will be called. -
Open app.component.ts and modify the code to include an import for ViewChild, add
a variable called child that uses the @ViewChild decorator and call the detectChanges method on the cdr
object contained in our child component.
import { Component, ViewChild } from '@angular/core'; import { HomeComponent } from './home/home.component'; import { IPerson } from './people.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent { public stringValue = 'Hello'; public personValue: IPerson = {nameFirst: 'John', nameLast: 'Simpson', age: 51, gender: 'Male'} @ViewChild(HomeComponent) child!: HomeComponent; updateChildren(): void { this.child.cdr.detectChanges(); } }
- View your application and type something into the personValue nameLast input field. You should see the personInput value update as you type inside your home component.
As you work with Angular you will find many other uses for ViewChild. In this example we used it to force change detection however any public method or variable can be accessed using ViewChild.
I mentioned earlier that ViewChild allows access to a single component instance. We are going to test this in the following steps as it may help you later if you run into problems when developing your MPages.
-
Make a copy of the <app-home> line in app.component.html and paste it directly
below the line you just copied. We are going to use the same personValue object in our demonstration.
<h2>Home Level Output</h2> <app-home [stringInput]="stringValue" [personInput]="personValue"></app-home> <app-home [stringInput]="stringValue" [personInput]="personValue"></app-home>
- View your application and make changes to the personValue nameLast input field. You will see the changes happen only in the first instance of <app-home>.
-
ViewChild can access our components by unique names. Adding a unique name to any component in Angular simply requires placing a hashtag (#) followed by the name you wish to assign to your component. Your name cannot have any spaces.
Modify your second <app-home> element to include a name called #updateMe as follows.
<app-home #updateMe [stringInput]="stringValue" [personInput]="personValue"></app-home>
-
Open app.component.ts and change your ViewChild decorator to point to the "updateMe"
name instead of the HomeComponent class name.
@ViewChild('updateMe') child!: HomeComponent;
- View your application again and make changes to the nameLast field. You should now see the second instance update and the first will not trigger change detection.
ViewChildren
Our last example does a great job of updating a single instance of a child component but isn't exactly what we want. Ideally we want all of our child <app-home> components to update as we make changes. This is where ViewChildren comes in.
ViewChildren allows us to reference a collection of ViewChild objects of the same type as a QueryList object. A QueryList object is an unmodifiable list of items that Angular keeps up to date with application state changes. Documentation can be found at https://angular.io/api/core/QueryList. For our purposes we will use it to loop through each ViewChild object to fire change detection.
-
Open app.component.ts and make the modifications shown below.
import { Component, ViewChildren, QueryList } from '@angular/core'; import { HomeComponent } from './home/home.component'; import { IPerson } from './people.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent { public stringValue = 'Hello'; public personValue: IPerson = {nameFirst: 'John', nameLast: 'Simpson', age: 51, gender: 'Male'} @ViewChildren(HomeComponent) children!: QueryList<HomeComponent>; updateChildren(): void { this.children.forEach((child: HomeComponent) => { child.cdr.detectChanges(); }); } }
We have removed our ViewChild import and added ViewChildren and QueryList. The @ViewChild decorated variable child has been replaced with a @ViewChildren decorated children object that is a QueryList of HomeComponent objects.
In our updateChildren method we loop through each child inside our children QueryList calling the cdr.detectChanges() method.
- View your application and make changes to the nameLast value. You should now see all instances of your <app-home> component update.
Summary
Change detection is one of the more difficult concepts you will encounter when working with Angular. As a new developer you may choose to use the default change detection in your MPages and that is perfectly okay. You can write advanced Angular MPages without playing with change detection that perform well. However, if you want to really fine tune the performance of your Angular MPages, implementing the change detection strategies described in this lesson will make a noticeable difference.