In this post we'll shed some light on the ngOnChanges() lifecycle hook in Angular: what it is, how it works and how we can make use of it within our Web Application - be it a SPA, NWE or PWA - to control the data shown to the client and also some specific interactions between the user and the client.
What's a Lifecycle Hook?
Before starting, it can be wise to spend a couple minutes to understand what lifecycle hooks are and the role they play in Angular.
As you might already know, an Angular app is basically made of Components. Each one of them has a lifecycle which is managed by Angular, meaning that Angular creates it, renders it, creates and renders its children, checks it when its data-bound properties change, and - eventually - destroys it before removing it from the DOM.
The Angular framework offers various lifecycle hooks that provide visibility into these key life moments and the ability to act when they occur: in other words, we could say that a lifecycle hook is an event that will trigger whenever a specific situation happens to the Component during its lifecycle. We're talking about Components for simplicity, but the same could be said for Directives, as they have the same set of lifecycle hooks.
ngOnChanges
As the Angular core docs clearly states, the ngOnChanges() method is a lifecycle hook that will trigger each time Angular sets a data-bound input property. That means that it should be used whenever we need something to happen whenever that property value changes. This can happen for a number of different reasons, such as: user interaction, user-driven or app-driven async call, and so on.
Let's take the following Angular Component class as an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
import { Component, Inject, Input, OnChanges, SimpleChanges } from "@angular/core"; import { Router } from "@angular/router"; import { HttpClient } from "@angular/common/http"; @Component({ selector: "question-list", templateUrl: './question-list.component.html', styleUrls: ['./question-list.component.css'] }) export class QuestionListComponent implements OnChanges { @Input() quiz: Quiz; questions: Question[]; title: string; constructor(private http: HttpClient, @Inject('BASE_URL') private baseUrl: string, private router: Router) { this.questions = []; } ngOnChanges(changes: SimpleChanges) { if (typeof changes['quiz'] !== "undefined") { // retrieve the quiz variable change info var change = changes['quiz']; // only perform the task if the value has been changed if (!change.isFirstChange()) { // execute the Http request and retrieve the result this.loadData(); } } } loadData() { var url = this.baseUrl + "api/question/All/" + this.quiz.Id; this.http.get<Question[]>(url).subscribe(res => { this.questions = res; }, error => console.error(error)); } onCreate() { this.router.navigate(["/question/create", this.quiz.Id]); } onEdit(question: Question) { this.router.navigate(["/question/edit", question.Id]); } onDelete(question: Question) { if (confirm("Do you really want to delete this question?")) { var url = this.baseUrl + "api/question/" + question.Id; this.http .delete(url) .subscribe(res => { console.log("Question " + question.Id + " has been deleted."); // refresh the question list this.loadData(); }, error => console.log(error)); } } } |
This QuestionListComponent TypeScript class is part of the TestMakerFree sample project, a sample Angular app developed through the course of the ASP.NET Core 2 and Angular 5 book written by Valerio De Sanctis and published by Packt Publishing. The full source code is available on GitHub.
It goes without saying that such Component is not usable out of this context, however we just have to focus on lines 23-35 (the highlighted ones) when we make use of the ngOnChanges lifecycle hook through the ngOnChanges() method.
As we can see, we're using that hook to perform a specific async task - fetch some data through the server by calling the loadData() method - everytime the quiz input property value changes. It's worth noting that we're not monitoring the quiz property itself: our check is bound to the ngOnChanges lifecycle hook, which affects the whole Component each time a change is detected in one of its properties' value.
We had to use the ngOnChanges lifecycle hook instead of the constructor or ngOnInit() methods for a rather obvious reason: since our sample Component is meant to show a list of questions related to a quiz, we can't load the questions unless the parent quiz property is available, which is set asynchronously by the parent component: for this very reason, we need to defer the Http server call that will fetch the related questions until the parent quiz has been set.
Long story short, we're dealing with two async calls that must be executed one after another: since the quiz is also retrieved with an asynchronous Http call by the parent component, we had to find a way to tell our QuestionListComponent when the data-bound property is actually updated: this is when the ngOnChanges lifecycle hook comes to help.
The following diagram could be useful to better visualize this scenario:
The rounded rectangle with grey background is the asynchronous thread where the QuizEditComponent - the parent component - retrieves the quiz using the HttpClient.
By looking at the upper portion of the above schema we can see how, without using ngOnChanges(), the QuestionListComponent would issue a completely useless Http call trying to get the questions of an empty quiz object, thus getting zero results; besides, when the quiz is actually retrieved, it won't do anything because there are no triggers that could tell that the quiz object value has been changed: long story short, we would never get these questions.
The lower portion of the schema tells a whole different story: there are no useless Http calls at the end of the constructor-based lifecycle, as the isFirstChange() method that we put within our ngOnChanges() implementation will return TRUE, giving us a good reason to ignore the event and do nothing; later on, when the parent's async call will complete and the quiz will be set, that same method will return FALSE, giving the green flag for issuing the Http call the right way--or, to better say, at the right time.
This is a legitimate question in terms of code optimization: we can definitely afford to have a component that will serve all the available questions for a given quiz without knowing anything about the quiz itself, other than its ID. We could even say that, for the time being, working with the quiz ID and forget about the rest would be the right thing to do here. However, there will be other scenarios where we would need to have more info from our source object - or from our parent component - than those we could fetch from the parent route. Sooner or later we'll definitely hit one of them: when it will happen, we'll love to know that we could do something better than just issuing another Http call and re-fetch it all from scratch.
However, luckily enough, this is a whole different story....
Conclusion
The ngOnChanges() lifecycle hook is going to be a powerful tool in our Angular arsenal: learning how it works and how to use it can be a great help to overcome a number of nasty concurrency issues between components and is definitely a great addition in our Angular knowledge.
For further info regarding it, we strongly suggest to look at the following URL addresses from the Angular official documentation:
Really nice and informative article. Please keep writing good stuff. Good luck
Thank you a lot! If you liked it, give us a like on FB :)