Welcome back to our blog series, Exploring Angular Lifecycle Hooks!
Available Lifecycle Hooks covered in this series:
Let’s continue the series with one of the under-utilized, yet extremely helpful hooks, ngOnChanges
.
According to the Angular Docs, OnChanges
is used to “Respond when Angular (re)sets data-bound input properties. The method receives a SimpleChanges object of current and previous property values. Called before ngOnInit() and whenever one or more data-bound input properties change.”
Table of contents
In plain English, this lifecycle hook will allow us to monitor the value of Input
s to our components
and directives
and allow us to branch our logic to react differently when those values do change.
In this article, we will review how to implement OnChanges
, a common use case for OnChanges
, and a potential alternative using setters.
Angular ngOnChanges
OnChanges
is an Angular lifecycle method, that can be hooked into components
and directives
in Angular. By defining a specific method named ngOnChanges
on our class, we are letting the Angular runtime know that it should call our method at the appropriate time. This allows us to implement logic in our classes to handle updates to our changing Input
data.
Implementing OnChanges
In order to implement OnChanges
, we will follow two simple steps.
Free eBook
Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- Setters and Getters for Styles and Class Bindings
Add OnChanges after the implements keyword
The first step to implementing OnChanges
is to add OnChanges
after the implements
keyword on a component
or directive
.
Here’s a common component that lacks lifecycle hooks:
import { Component } from '@angular/core';
@Component({...})
export class SomeCoolComponent {}
Let’s import OnChanges
from Angular’s core package. Once imported we can create a contract with implements OnChanges
:
import { Component, OnChanges } from '@angular/core';
@Component({...})
export class SomeCoolComponent implements OnChanges {}
Fun Fact Time: Technically it’s not required to implement the interface, Angular will call ngOnChanges regardless, however, it’s very helpful for type-checking, and to allow other developers to quickly identify which lifecycle hooks are in use on this class.
Add the ngOnChanges method to our class
With our newly added OnChanges
after implements
TypeScript IntelliSense will underline the class declaration in red, giving a warning that ngOnChanges
was not found. We can resolve that issue by creating our ngOnChanges
method.
Example Component Before:
import { Component, OnChanges } from '@angular/core';
@Component({...})
export class SomeCoolComponent implements OnChanges {}
Example Component After:
import { Component, OnChanges } from '@angular/core';
@Component({...})
export class SomeCoolComponent implements OnChanges {
ngOnChanges(changes: SimpleChanges) {
// Input change handling logic goes here
}
}
The SimpleChanges Object
As you can see above, the ngOnChanges
method takes in a changes: SimpleChanges
parameter. SimpleChanges
is an object that will have a property for each Input
defined in your component
or directive
.
Here is the shape of the SimpleChanges
object:
interface SimpleChanges {
[propName: string]: SimpleChange;
}
Each property defined in SimpleChanges
will have a child SimpleChange
object:
interface SimpleChange {
currentValue: any;
previousValue: any;
firstChange: boolean;
isFirstChange(): boolean;
}
currentValue
- This property will contain the value of theInput
at the time this method was firedfirstChange
- This property will contain a boolean value of whether or not this is the first time the value has changed. The first time a value comes through anInput
counts as a “change” and therefore will reflect true here. Subsequent changes will be false. This can be helpful if yourcomponent
ordirective
needs to behave differently based on when the value changed.previousValue
- This property will contain the last value of theInput
before this change happened. This can be helpful when comparing current to previous values, especially if you need to display to the user a “before” and “after” state.isFirstChange()
- This is a helper method that returnstrue
if this is the first time this value has changed.
As you can see, the SimpleChange
object can be really helpful. It allows us to inspect the changes flowing through ngOnChanges
and make intelligent decisions in our logic based on the values in this object.
OnChanges In The Real World
Implementing OnChanges
was a simple two-step process. Let’s dive in and review a real-world use case for OnChanges
. At the beginning of the article, we mentioned that Angular recommends the following: “Respond when Angular (re)sets data-bound input properties. The method receives a SimpleChanges object of current and previous property values. Called before ngOnInit() and whenever one or more data-bound input properties change.”
Revisiting The Github Repository Explorer Example
Let’s revisit an example from my previous OnInit
article in this series, the Github Repository Explorer
.
If we remember correctly, we had a component named GithubReposComponent
, that had an Input
for the repoLimit
. In the example, we initialized our repos$
with a call to the GithubService.getMostStarredRepos
and passed in the repoLimit
.
Here’s the full component:
// github-repos.component.ts
import { Component, OnInit, Input } from '@angular/core';
import { Observable } from 'rxjs';
import { GithubService, GithubRepo } from './github.service';
@Component({
template: `
<app-github-repo
*ngFor="let repo of (repos$ | async)"
[githubRepo]="repo">
</app-github-repo>`
})
export class GithubReposComponent implements OnInit {
@Input() repoLimit: number;
repos$: Observable<GithubRepo[]>;
constructor(private githubService: GithubService) {}
ngOnInit() {
this.repos$ = this.githubService.getMostStarredRepos(this.repoLimit);
}
}
OnChanges, the hero we all need
If we are handling the repoLimit
Input
in the ngOnInit
, we might say to ourselves: “what’s the problem?” Well, the problem is that we only handle repoLimit
in the ngOnInit
. This means that if we were to have a new value flow down from the parent in the repoLimit
Input
our repos$
would not re-retrieve the new set of repos with the new limit.
How do we fix our component so that our repos$
are re-retrieved every time repoLimit
changes? Well, this is where our new hero, OnChanges
comes to the rescue.
Let’s implement OnChanges
and add our new ngOnChanges(changes: SimpleChanges)
method to our component. Inside that new method, let’s check for changes.repoLimit
to be truthy and if so, then let’s initialize our repos$
observable to a service call passing in the changes.repoLimit.currentValue
to retrieve the latest value for the repoLimit
Input
.
// github-repos.component.ts
import { Component, OnChanges, Input } from '@angular/core';
import { Observable } from 'rxjs';
import { GithubService, GithubRepo } from './github.service';
@Component({
template: `
<app-github-repo
*ngFor="let repo of (repos$ | async)"
[githubRepo]="repo">
</app-github-repo>`
})
export class GithubReposComponent implements OnChanges {
@Input() repoLimit: number;
repos$: Observable<GithubRepo[]>;
constructor(private githubService: GithubService) {}
ngOnChanges(changes: SimpleChanges) {
if (changes.repoLimit) {
this.repos$ = this.githubService.getMostStarredRepos(changes.repoLimit.currentValue);
}
}
}
Fantastic! Now our component will re-retrieve our repos$
every time repoLimit
changes.
Setters vs ngOnChanges
Reviewing the previous example, let’s refactor our component a bit more and make use of an alternative to OnChanges
that will also allow us to re-retrieve our repos$
each time repoLimit
changes. To do so, we will convert the repoLimit
Input
into a TypeScript setter
using the set
syntax.
Creating a refreshRepos Method
First, let’s create a new method named refreshRepos(limit: number)
and move the repos$
initialization into that new method. Our new refreshRepos
method should look like this:
refreshRepos(limit: number) {
this.repos$ = this.githubService.getMostStarredRepos(limit);
}
Removing the OnChanges Implementation
Next, let’s remove the OnChanges
implementation from our component, first removing the implements OnChanges
and then removing the ngOnChanges
method altogether.
Our class declaration will look something like this with OnChanges
and ngOnChanges
removed:
export class GithubReposComponent {...}
Converting the repoLimit Input to a setter
TypeScript setters provide a way to define a method that is called every time the value on the class is set or changed.
Now, let’s add a setter
to our Input() repoLimit: number
. In the set
for repoLimit
we will make a call to our refreshRepos
method passing in the newLimit
.
Our repoLimit
setter will look like this:
@Input() set repoLimit(newLimit: number) {
this.refreshRepos(newLimit);
}
A Refactored Component
Congratulations! We have completed refactoring our component to use a setter
instead of OnChanges
. This provides a simpler solution to our problem.
The finished component will look like this:
// github-repos.component.ts
import { Component, Input } from '@angular/core';
import { Observable } from 'rxjs';
import { GithubService, GithubRepo } from './github.service';
@Component({
template: `
<app-github-repo
*ngFor="let repo of (repos$ | async)"
[githubRepo]="repo">
</app-github-repo>`
})
export class GithubReposComponent {
@Input() set repoLimit(newLimit: number) {
this.refreshRepos(newLimit);
}
repos$: Observable<GithubRepo[]>;
constructor(private githubService: GithubService) {}
refreshRepos(limit: number) {
this.repos$ = this.githubService.getMostStarredRepos(limit);
}
}
As we review the above example, we might ask ourselves, does this still work on initialization? Well, the answer is yes! This is because the repoLimit
setter
is called when the Input
is first set, and then every time thereafter it is changed.
Conclusion
Well, folks, we have reached the end of another article in this series on Angular Lifecycle hooks! If you take anything away from this article, I hope it is that OnChanges
is powerful, but should be used wisely. And maybe, just maybe, you should consider using TypeScript setters instead.