Welcome back to our blog series, Exploring Angular Lifecycle Hooks!
Let’s continue the series with one of the most widely used hooks, ngOnInit.
Table of contents
Available Lifecycle Hooks covered in this series:
OnInit
’s primary purpose, according to the Angular Docs is to “Initialize the directive/component after Angular first displays the data-bound properties and sets the directive/component’s input properties. Called once, after the first ngOnChanges().”
First impressions are interesting because, going by the name alone, you would expect the OnInit
hook to be executed first when the component is mounted. That almost is the case, but our friend OnChanges
actually runs just before OnInit
!
You may be asking yourself; what are some good use cases for OnInit
? Why can’t I just put my initialization logic in the class constructor
? Should class property assignments happen here or can they just happen at variable declaration?
In this article, we will review how to implement OnInit
, common use cases for OnInit
, and wrap-up with some bonus use cases for OnInit
to answer all those questions.
Angular ngOnInit
OnInit
is an Angular lifecycle method, that can be hooked into components
and directives
in Angular. By defining a specific method named ngOnInit
on our class, we are telling the Angular runtime, that it should call our method at the appropriate time. This is a powerful and declarative way to add specific initialization logic near the beginning of our class lifecycle.
Implementing OnInit
As with other Angular lifecycle methods, adding the actual hook for OnInit
is relatively simple.
Add OnInit after the implements keyword
The first step to implementing OnInit
is to add OnInit
after the implements
keyword on a component
or directive
.
Here’s a typical component without any lifecycle hooks:
import { Component } from '@angular/core';
@Component({...})
export class MyValueComponent {}
Our first change is to import OnInit
from Angular’s core and then create a contract with implements OnInit
:
import { Component, OnInit } from '@angular/core';
@Component({...})
export class MyValueComponent implements OnInit {}
Fun Fact Time: Technically it’s not required to implement the interface, Angular will call
ngOnInit
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 ngOnInit method to our class
Now that we have added the OnInit
after implements
the TypeScript intellisense will underline the class declaration in red, giving a warning that ngOnInit
was not found. Let’s fix that by creating our new ngOnInit
method.
Example Component Before:
import { Component, OnInit } from '@angular/core';
@Component({...})
export class MyValueComponent implements OnInit {}
Example Component After:
import { Component, OnInit } from '@angular/core';
@Component({...})
export class MyValueComponent implements OnInit {
ngOnInit() {
// initialization logic goes here
}
}
You’ll also note that this lifecycle hook takes no arguments, unlike some of the others we’ll be covering in later articles.
Constructor vs OnInit
For a deep dive on Constructor vs OnInit checkout out our in-depth writeup here!
If you’re like me you’ve asked the question; should I place my initialization logic in the constructor
or should I place it in ngOnInit
? Well the answer, in short, is that we should place our initialization logic in ngOnInit
. The constructor
is best left up to the purpose of injecting dependencies, such as services, into our components.
Why? Well, to quote our in-depth writeup, “the JavaScript engine calls the constructor, not Angular directly. Which is why the ngOnInit
(and $onInit
in AngularJS) lifecycle hook was created.” In addition, “@Input() properties are available inside ngOnInit, however are undefined inside the constructor, by design”. This means we can access anything passed down into the component inside the ngOnInit
hook.
Common Use Cases
Implementing OnInit
was a simple two-step process. Let’s dive in and review some common use cases for OnInit
. At the beginning of the article, we mentioned that Angular recommends the following: “Initialize the directive/component after Angular first displays the data-bound properties and sets the directive/component’s input properties. Called once, after the first ngOnChanges().”
Let’s explore this further through an example.
A GitHub Repository Explorer
Let’s imagine we are tasked with building a GitHub Repository Explorer.
- This explorer should display the most starred repositories on GitHub with some details about each repository.
- The user should have the ability to limit how many repositories are returned.
- The user should have the ability to navigate to a repository detail page.
- The developer assigning this task has graciously provided a
GithubService
that has two public methods for getting back our data.
Awesome! We can make a call to this service, get back our list of repositories and render them to the browser.
Let’s take a journey through building out this feature in a series of steps that will explore the common use cases for ngOnInit
.
GitHub Service
Let’s explore the service a bit further, that has been provided to us.
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
- We can see that it has a public method named
getMostStarredRepos
that returns anObservable<GithubRepo[]>
which emits a list ofGithubRepo
objects. There is one argument togetMostStarredRepos
that limits the number of repos to return. If this argument is not provided, the value defaults to5
. - It also has a public method named
getRepoDetails
that returns anObservable<GithubRepoDetail>
given an argument ofid
// github.service.ts
@Injectable(..)
export class GithubService {
getMostStarredRepos(limit: number = 5): Observable<GithubRepo[]> {}
getRepoDetails(id: string): Observable<GithubRepoDetail> {}
}
Building Our Explorer
Let’s build scaffold out of two components, GithubReposComponent
which is our parent component and GithubRepoComponent
which will be our child component.
GithubReposComponent
will be responsible for subscribing to the results of GithubService.getMostStarredRepos
and passing the emitted GithubRepo
objects down to each GithubRepoComponent
to render repository information to the browser.
But, what is the best way to go about this? Shouldn’t we be able to just set a public class array property repos
to the return value of getMostStarredRepos
and loop over that value in our html template to render repository details for each item in the repos array?
Well, we are dealing with Observables here, so it’s not that simple.
Let’s imagine our parent, GithubReposComponent
, has a bare bones structure similar to the following, having already implemented OnInit
as described previously:
github-repos.component.ts
import { Component, OnInit } from '@angular/core';
import { GithubService } from './github.service';
@Component({
template: `<app-github-repo [githubRepo]="null"></app-github-repo>`
})
export class GithubReposComponent implements OnInit {
constructor(private githubService: GithubService) {}
ngOnInit() {}
}
Now, let’s imagine our child, GithubRepoComponent
, has a bare bones structure similar to the following:
// github-repo.component.ts
import { Component, Input } from '@angular/core';
@Component({
template: `{{ githubRepo | json }}`
})
export class GithubRepoComponent {
@Input() githubRepo: GithubRepo;
}
Finally, we have our detail page, GithubRepoDetailComponent
component, for displaying repo details when navigated to. It is not currently wired up to pull parameters from the route, so you will notice that repoId
is being set to an empty string. As we walk through ngOnInit
we will fix that.
// github-repo-detail.component.ts
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { GithubService, GithubRepoDetails } from './github.service';
@Component({
template: `
{{ githubRepoDetails$ | async | json }}
`
})
export class GithubRepoDetailComponent implements OnInit {
githubRepoDetails$: Observable<GithubRepoDetails>;
constructor(private githubService: GithubService) {}
ngOnInit() {
const repoId = '';
this.githubRepoDetails$ = this.githubService.getRepoDetails(repoId);
}
}
Let’s explore together how to wire these two components together to complete a simple Github Explorer feature as requested.
Observables and ngOnInit
One of the most common use cases for ngOnInit
, is to establish observable assignments to component fields. It’s good practice to do this in the ngOnInit
so that observables are initialized at a predictable time in the component lifecycle. This exercises good code manners as other developers have a common place to find component field assignments.
Continuing with our Github Repository Example, we need to subscribe to the return of the GithubService.getMostStarredRepos
method so that we can iterate over the result and render a list of repositories, passing each GithubRepo
object to an instance of the GithubRepoComponent
child component. Remember, our GithubRepoComponent
component has an input property named githubRepo
.
Now, let’s proceed with creating our Observable
property and wiring up the call to the service:
- Import
Observable
fromrxjs
- Import
GithubRepo
from./github.service
- Declare a new
repos$
Observable property - Set
repos$
equal to the return ofGithubService.getMostStarredRepos
- Update our template to subscribe to
repos$
with theasync
pipe - Convert our template to use
*ngFor
to produce aapp-github-repo
for each item
// github-repos.component.ts
import { Component, OnInit } 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 {
repos$: Observable<GithubRepo[]>;
constructor(private githubService: GithubService) {}
ngOnInit() {
this.repos$ = this.githubService.getMostStarredRepos();
}
}
Great! Now you have learned the most common use case for ngOnInit
, wiring up observables. When our app runs, the parent component template will subscribe to our service and render a list of repos to the browser.
Input Properties with OnInit
Another common use case for ngOnInit
is to wire up observable fields here that depend on Input
properties. This is because by the time ngOnInit
fires, the Input
properties are available to the component. If we were to try and access these in the constructor
they would return the dreaded undefined
.
Getting back to our example, one of the original requirements of the explorer was that we must give the user “the ability to limit how many repositories are returned.”
To meet that requirement, let’s add an input property to our GithubReposComponent
component.
- Add the
Input
decorator to our imports - Declare a new class field
@Input() repoLimit: number
- Inside
ngOnInit
passrepoLimit
to ourgetMostStarredRepos
call
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);
}
}
Now, when the GithubReposComponent
is rendered, the repos will be limited to the limit
input being passed in to the parent component. This might look something like the following if GithubReposComponent
were in our AppComponent
:
app.component.html
<app-github-repos [repoLimit]="'5'"></app-github-repos>
Route Parameters inside OnInit
Most Angular applications employ the Router
and as such, there are situations where you may need to retrieve parameters from that route. The recommended way to do so in Angular, is to subscribe to the ActivatedRoute.ParamMap
. This returns an Observable
of params
that we can subscribe to and pull out values from the route.
Returning to our Github Repository Explorer example, one of the requirements was that the “user should have the ability to navigate to a repository detail page.”.
To meet that requirement, let’s imagine our GithubRepoComponent
child component had a routerLink
that routed to a new GithubRepoDetailComponent
.
Let’s quickly update our GithubRepoComponent
to route to the detail component:
- Add a new hyperlink to our
template
that usesrouterLink
to send the user to thedetail
route withgithubRepo.id
as therouteParam
// github-repo.component.ts
import { Component, Input } from '@angular/core';
@Component({
template: `
{{ githubRepo | json }}
<a routerLink="/detail/{{ githubRepo.id }}">Details</a>
`
})
export class GithubRepoComponent {
@Input() githubRepo: GithubRepo;
}
Jumping over to our GithubRepoDetailComponent
, let’s fix the issue of repoId
being set to an empty string, and actually retrieve the id
parameter from the route in our ngOnInit
.
To do this we need to take the following steps:
- Import
ActivatedRoute
andParamMap
from@angular/router
- Import
switchMap
fromrxjs/operators
- Inject
ActivatedRoute
in the constructor asprivate route: ActivatedRoute
- Modify our
githubRepoDetails$
Observable
to pull theid
route parameter from the route using thethis.route.paramMap
Observable
and usingswitchMap
to combine the streams together for a final result.
// github-repo-detail.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Observable } from 'rxjs';
import { GithubService, GithubRepoDetails } from './github.service';
@Component({
template: `
{{ githubRepoDetails$ | async | json }}
`
})
export class GithubRepoDetailComponent implements OnInit {
githubRepoDetails$: Observable<GithubRepoDetails>;
constructor(private githubService: GithubService, private route: ActivatedRoute) {}
ngOnInit() {
this.githubRepoDetails$ = this.route.paramMap.pipe(
switchMap((params: ParamMap) => this.githubService.getRepoDetails(params.get('id')))
);
}
}
Now anytime we route to the detail page, our ngOnInit
will handle setting up the Observable
to pull the id
parameter from the ActivatedRoute
and wire-up the Observable
to retrieve the details for the correct GitHub Repository.
Using ngOnInit in other ways
Manual Subscriptions in Tandem with OnDestroy
Subscribing via the
async
pipe in templates is the preferred method, as Angular manages subscribing and unsubscribing automagically for you!
There are some cases where you need to manually subscribe to Observable
s inside your component class. If you are doing so, ngOnInit
is a great place to do that.
Let’s imagine the following component needs to setup a manual subscription to a keydown
event and log to the console on each event.
import { Component, OnInit } from '@angular/core';
import { fromEvent, Subscription } from 'rxjs';
@Component({...})
export class MyKeyDownComponent implements OnInit {
ngOnInit() {
fromEvent(document, 'keydown').subscribe(event => console.log({event}));
}
}
Reflecting back to my previous OnDestroy
article in this series, it’s considered good practice to always unsubscribe from subscriptions to prevent memory leaks.
Keeping that best practice, let’s capture the subscription in our ngOnInit
so that we can unsubscribe in our ngOnDestroy
:
import { Component, OnInit, OnDestroy } from '@angular/core';
import { fromEvent, Subscription } from 'rxjs';
@Component({...})
export class MyKeyDownComponent implements OnInit, OnDestroy {
keyDownSub: Subscription;
ngOnInit() {
this.keyDownSub = fromEvent(document, 'keydown').subscribe(event => console.log({event}));
}
ngOnDestroy() {
if (this.keyDownSub) {
this.keyDownSub.unsubscribe();
}
}
}
Now you can see clearly, how ngOnInit
can be used in tandem with ngOnDestroy
to properly create and destroy subscriptions for effective prevention of memory leaks.
Asynchronous OnInit
Just as with other lifecycle methods, with ngOnInit
you can add async
in front of the method name to make use of the async/await
syntax in modern JavaScript/TypeScript.
Reimagining our Github Repository Explorer, we can deal with services that return a Promise
by awaiting the method call.
// github-repos-promise.component.ts
import { Component, OnInit, Input } from '@angular/core';
import { GithubPromiseService, GithubRepo } from './github.service';
@Component({
template: `
<app-github-repo
*ngFor="let repo of repos"
[githubRepo]="repo">
</app-github-repo>`
})
export class GithubReposPromiseComponent implements OnInit {
@Input() repoLimit: number;
repos: GithubRepo;
constructor(private githubPromiseService: GithubPromiseService) {}
async ngOnInit() {
try {
this.repos = await this.githubPromiseService.getMostStarredRepos(this.repoLimit);
} catch (error) {
console.error({error});
}
}
}
Reactive Forms and OnInit
On many occasions when using Reactive Forms in Angular, we need to construct complex FormGroup
objects using the FormBuilder
service. More often than not, our form fields rely on the data being passed in through Input
properties. If we are constructing our FormGroup
inside the constructor
then this can be problematic as our Input
properties will be undefined
.
For safety and consistency, we should get in the habit of building our FormGroup
inside ngOnInit
.
Take, for example, the following MyFormComponent
.
You will notice that buildForm
is creating and initializing the form: FormGroup
with a field that has defaultDescription
set as the initial value.
Can you spot a potential bug?
// my-form.component.ts
import { Component, Input } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
@Component({...})
export class MyFormComponent {
@Input() defaultDescription: string;
form: FormGroup;
constructor(private fb: FormBuilder) {
this.buildForm();
}
buildForm() {
this.form = this.fb.group({
description: [this.defaultDescription]
});
}
}
You guessed it! Our defaultDescription
may be undefined
and therefore incorrectly initialize the description
field on the form.
Let’s make a small tweak to the component to utilize OnInit
so that we can ensure our defaultDescription
Input
has a value before building the form.
// my-form.component.ts
import { Component, OnInit, Input } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
@Component({...})
export class MyFormComponent implements OnInit {
@Input() defaultDescription: string;
form: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.buildForm();
}
buildForm() {
this.form = this.fb.group({
description: [this.defaultDescription]
});
}
}
As you can see, with just a small tweak to our component, we have corrected a potential bug. The Reactive Forms example above also follows the principle of using ngOnInit
for accessing properties, I’ve seen many code examples that don’t follow it.
Conclusion
That brings us to the end of the article! Hopefully you’ve been able to glean some good advice on why and how to use OnInit
logic in your applications.
It would also be wrong to get through an entire article without mentioning testing of some kind! Using the appropriate approaches described above will make your tests safer and more easy to test, for example you can invoke the ngOnInit
method yourself and test the outcome.
I will leave you with a couple best practices that should be adopted:
- Always implement the
OnInit
interface - Always assign observables in the
ngOnInit
method as good code manners - “If In Doubt, OnInit It Out!” avoid the
constructor
where appropriate.
To learn more techniques, best practices and real-world expert knowledge I’d highly recommend checking out my Angular courses - they will guide you through your journey to mastering Angular to the fullest!