Component initialization without ngOnInit with async pipes for Observables and ngOnChanges
Explore various techniques to improve initlialization code in components. We will replace the ngOnInit entirely and propose better alternatives. We will cover subscriptions management, and other life cycle hooks, such as ngOnChanges.

The ngOnInit
hook has always been a part of the component’s generate script when using Angular CLI. Right now we are not so sure if it still will be included by default in the next versions of Angular. Recently in the Angular community, there are mentions of removing this hook declaration from the default Angular CLI generator. I've led to this poll, created by Angular, which at the moment, shows an almost 50:50 split regarding whether component generation should include ngOnInit
hook or not!
The whole idea of removing the ngOnInit
which we are all familiar with may seem strange at first, but there’s definitely more to it. In this article, I will present to you why the ngOnInit
is not needed in many cases, how we can refactor our existing code and why in many enterprise applications this hook is used very rarely.
I will make my best to convince you that:
- you don’t need the
ngOnInit
when dealing with Observables - most of the time,
ngOnChanges
is a better alternative forngOnInit
We will also learn what is the difference between the ngOnInit
and the constructor
.
The ngOnInit
We got used to the idea that ngOnInit
is always available for us, ready to be filled with tons of lines of code to make the component work. This led to a bunch of misunderstandings, misused features, and the spreading of anti-patterns in the codebase.
Angular docs define the OnInit
hook as:
A lifecycle hook that is called after Angular has initialized all data-bound properties of a directive. Define an ngOnInit()
method to handle any additional initialization tasks.
This doesn’t mean all initialization tasks should be put there. We may think, that there are a lot of tasks that we don’t want to put in the constructor due to some complex or heavy logic. However quite often if we use other hooks correctly, take advantage of Observables or even get some help from the NgRx, our logic will be simplified and handled in better places than ngOnInit
.
Finally, it’s essential to realize that often ngOnInit
is overloaded with function calls, subscriptions, the code blends template, and class responsibilities resulting in complex and unreadable setup code.
The component’s code should be all about simplicity and readability. We can break components into smaller ones when needed, we can take advantage of pipes, directives, various lifecycle hooks, and Observables so we are blessed with various techniques to implement clean code in Angular.
In the following sections, I will present to you, some use cases of using ngOnInit
when there is really no need for that, and propose other ways to solve the problems.
Why couldn’t we use a constructor instead?
It may look like, we could move everything from ngOnInit
to the constructor
block, and call it a day. Well, it’s not that simple.
There is a big difference between the constructor
and the ngOnInit
. Max wrote a great article about this, which you may find here. For us, the most important thing is, the constructor is called when Angular constructs the components tree, while ngOnInit
is run after the change detection when all bindings are updated. Construction time is when the dependency injection takes place because at this point Angular has already configured the root module injector and all of the parent injectors. This is not the time when DOM is ready and we can access input properties, elements, and child components. Properties such as input properties, static children will be accessible in the ngOnInit
. Other non-static elements will be available in onAfterViewInit
hook.
That’s why we can’t put any initialization logic which relies on DOM, elements, input data, bindings, etc in the constructor
— because they are not yet available. For most cases setup logic in construction, time is limited to dependency injection things and routing.
Subscriptions
Components are often using some kind of asynchronous way to get the data it then uses to display. This happens because many things in Angular use Observables, like for instance the HttpClient
. What we can do with the data delivered asynchronously? We can treat them as they are delivered, so as Observables, subscribe to them, and get the value. The other option could be to convert them to Promises and resolve the value by awaiting it.
If your reason to use ngOnInit
is only for subscriptions, then consider this approach which might be a better option.
While many people use the Observables they don’t take full advantage of going this way. The Observable is being subscribed as soon as possible, and the retrieved data is assigned to the component’s property. This process usually takes place in the ngOnInit
method. I’ve seen snippets as below many times during the code reviews:
data: MyData;
constructor(private readonly dataService: DataService) { }
ngOnInit() {
this.dataService.getData().subscribe(
data => this.data = data,
);
}
Although this technique works and the data
property can be used in the template to render the result, it has some pitfalls on the way.
The data property is indeed not typed correctly - during the time between component creation, and data being emitted by the Observable, the actual value is undefined
. So the correct typing should be:
data: MyData | undefined;
// or
data?: MyData;
This is a bit misleading, we don’t want this property to be undefined, but we don’t know when it will have the value, so in fact, it is an optional property.
You may also specify directly the default value that will be typed correctly:
data: MyData | null = null
// or
data: MyData = InitialMyData; // some object with init state
Subscriptions – each time we subscribe within directives we should remember to unsubscribe from them to avoid possible memory leaks. This makes code much bigger and populated with boilerplate:
data: MyData;
private readonly onDestroy = new Subject();
ngOnInit() {
this.dataService.getData()
.pipe(
takeUntil(this.onDestroy)
).subscribe(
data => this.data = data,
);
}
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
We have no idea when the value of the data
will be delivered. This leads to some serious troubles when we need to use the value in the component’s class. Imagine some of the component’s methods actually use this.data
to process something — and we can’t guarantee the value will be present at this point!
Another problem is the assumption that values are delivered synchornously, which maybe true in some specific cases, it may work, but usually it won’t work, and it will cause a lot of bugs. Below is an example snippet, I’ve seen a lot during the code reviews:
data: MyData;
someValue: string;
ngOnInit() {
this.dataService.getData().subscribe(
data => this.data = data,
);
// this.data will be most likely undefined at this point
// and will throw an Error
this.someValue = this.data.value;
}
We can move the line with someValue
assignment to the subscribe block, but it only resolves this single issue, it won’t save us from possible future mistakes like this.
Refactor of the subscriptions!
In Angular we are not limited to use only plain static values in the templates. We can use the Observables directly in the template. In other words, it’s rarely a need to subscribe to the Observable in the component’s class.
At first glance, it may seem tricky and we may struggle with understanding the “stream” way of handling and processing the values, but once you understand that, it will become very straightforward to use.
We should subscribe when we actually need the value. Do we need it in the component’s class?
The answer is - not really! We actually need the value in the template so we should subscribe there. Angular provides us, with an incredibly useful pipe — the AsyncPipe
, which subscribes to the Observable and returns emitting values, it also takes care of unsubscribing on component’s destroy. Please note, this is not better in case of performance in any way, than a simple subscribe call or other techniques — but in our case, it makes the component far more readable and makes ngOnInit
redundant.
Take a look at the following component’s class:
@Component({...})
export class Component {
readonly data$ = this.dataService.getData();
constructor(private readonly dataService: DataService) { }
}
It does not have the ngOnInit
hook implemented!
While this reduces the lines of code, it has other, much greater advantages than the subscribe technique from the example before.
- component’s property
data$
has a value from the start, the Observable is assigned at constructor time. The property is no longer optional. - because the property is assigned during the component’s creation, the property can be made
readonly
— which will guarantee, that nothing will replace its value. - there is no subscription (in the component’s class), so no need to unsubscribe, and the
ngOnDestroy
hook - we don’t have any enigmatic properties in the component, about which we have no idea when they will be assigned, and when they can be used for processing or any other logic.
The actual usage (subscription) is implemented in the template — where we actually need the data. Take a look at the following template snippet:
<p> {{ (data$ | async).someValue }} </p>
The content of the paragraph will be rendered with the value of the emitted event from the `data$` Observable.
You have to admit this way of handling Observables is much more readable, and it has far fewer moments where we can make a mistake and misunderstand the asynchronous code.
Shouldn’t the constructor only be used for DI?
Yeah, but please note, that when we assign the Observable to the component’s property, we don’t actually do anything with it (no logic there), we will not receive and process any values, as long as we don’t subscribe to it!
We are subscribing in the template (via AsyncPipe
), not in the constructor — so none of our (possibly complex) logic will be run at the component’s creation.
Data processing and nested subscriptions
You may say, that although the technique from above works pretty cool, it won’t work when we need to process the data, or combine data from various sources together, or maybe even that although it works, it is quite inconvenient to put | async
everywhere in the template, where we need the value.
Luckily, there are answers for these more advanced usages. We will tackle them one by one!
Data processing
Let us start with an example for processing the data. We may end up in the situation the API returns a large object, while we only care about some specific fragment, and we need this fragment to be in a slightly different shape than provided by the API. For instance, assume, the API delivers an object of such structure:
interface MyDTO {
data: {
name: string;
time: string;
}[]
}
So it’s an object, with an array of objects which includes name
and time
values. Assume, that for some reason, we only care about the first item in the array, and additionally, we need to convert the time
from a string
into a proper Date
object.
The following snippet presents how this can be tackled in “subscription” world.
data?: { name: string, time: Date };
constructor(private readonly dataService: DataService) { }
ngOnInit() {
this.dataService.getData().subscribe(
response => {
const first = response.data[0];
this.data = {
name: first.name,
time: new Date(first.time)
}
}
);
}
It does not contain the unsubscribe logic for readability, but you get the idea.
We subscribe for the data, and once it's emitted we create an object, with two properties which we are creating based on the emitted event called response
.
Refactor of the data processing!
How we can refactor this, so we use composition techniques, similar to functional programming and streams processing? The strategy for such problems is quite simple — we have to stop thinking about operating on the actually resolved values, but rather transform a stream of data, creating rules which will be triggered once the data will be emitted — so available for processing.
As you know, In RxJs we are using functions called pipeable operators, which are wrapped in the pipe
, and make it possible to transform the stream of values into another stream. Each pipeable operator gets the Observable, applies some logic, and returns a new Observable, which is handled by the operator next, in the pipe.
One of these operators is called map
, allowing us to transform values emitted by the source Observable using the provided function. You can read more about the map
operator here — and we are jumping straight to the implementation.
readonly data$ = this.dataService.getData().pipe(
map((response) => {
const first = response.data[0];
return {
name: response.name,
time: new Date(response.time)
}
}),
);
constructor(private readonly dataService: DataService) { }
We have added the pipe
function which includes the pipeable operators — in our case the map
operator. This operator contains a function that defines how we want to transform the stream. Here the transform function is identical to one in the “subscription” way:
response => {
const first = response.data[0];
return {
name: response.name,
time: new Date(response.time)
}
}
We transform the response, into an object made from the first item in the array, and take its properties.
It’s quite easy to use it in the template:
<p> {{ (data$ | async).name }} </p>
Nested Observables - nested subscriptions?
Another common situation is when our call to API relies on the data from the routing — for instance, an ID of something. Imagine such a route, where 1257623
is an ID of a hero:
localhost:4200/hero/1257623
We have to get the ID from the routing, and then call the API for the hero data. The snippet below presents how this can be implemented using the subscription:
hero?: { name: string };
constructor(
private readonly route: ActivatedRoute,
private readonly heroService: HeroService,
) { }
ngOnInit() {
const id = this.route.snapshot.params.id;
this.heroService.getHero(id).subscribe(
response => this.hero = response
);
}
What could go wrong with such implementation? When we update the app URL with a new ID, the component will still display the old hero, and it won’t call API to get data for a new ID. Why does it work that way? The component gets id from the route
snapshot
on init — and it will not care about any other values in the future. HeroService
won’t be called again, the ngOnInit
will not be called again — the component is already displayed and ngOnInit
happens only once.
This mechanism design may be hard to understand at first glance, so if you are interested in this, here you may find more details about this.
We could improve our solution to work with params changes — via getting params as Observable:
hero?: { name: string };
constructor(
private readonly route: ActivatedRoute,
private readonly heroService: HeroService,
) { }
ngOnInit() {
this.route.params.subscribe(
(params) = > {
const id = params.id;
this.heroService.getHero(id).subscribe(
response => this.hero = response
);
}
)
}
The code grows rapidly, and due to nesting, it becomes harder to understand (and we still didn’t include unsubscribe logic here!).
Refactor of the nested streams!
The situation may look different from the example about data processing — but in a way, it is really the same problem. We need to transform the Observable. Before we had to transform the actual response, here we need to combine two Observables.
We have to use the same flatteting technique with pipeable operators — this time though, we need to use a different operator. The one that we will use is called switchMap
and it allows us to map a value from a higher order Observable to a different Observable. You can read more about switchMap
here. In our case the implementation can look as follows:
readonly hero$ = this.route.params.pipe(
switchMap(params => this.heroService.getHero(params.id))
);
constructor(
private readonly route: ActivatedRoute,
private readonly heroService: HeroService,
) { }
I’m pretty sure that the template code is obvious at this point:
<p> {{ (hero$ | async).name }} </p>
View Model — vm$
The last problem in this section refers to too many | async
calls in the template which can obfuscate the template or even result in some performance issues.
This problem can happen for two reasons:
We use a single Observable in many elements, for instance:
<p> {{ (hero$ | async).name }} </p>
<p> {{ (hero$ | async).surname }} </p>
<p> {{ (hero$ | async).city }} </p>
We have many Observables in the component, for instance:
readonly hero$ = this.route.params.pipe(
switchMap(params => this.heroService.getHero(params.id))
);
readonly pet$ = this.route.params.pipe(
switchMap(params => this.heroService.getPet(params.id))
);
readonly cities$ = this.heroService.getCities();
constructor(
private readonly route: ActivatedRoute,
private readonly heroService: HeroService,
) { }
To tackle that we have to dive into the NgIf
directive — provided by Angular. We usually use the ngIf
in the template to show/hide some elements depending on some condition. NgIf
allows us to persist the result of the condition in a local template variable using the as
keyword. The following snippet presents the syntax:
<p *ngIf="hero.isAlive as alive"> {{ alive }} </p>
In the paragraph content, we were able to use the variable value, instead of the whole hero.isAlive
syntax.
We can use this technique to persist the value resolved from the `AsyncPIpe` and use it in many elements while subscribing only once.
Refactor for the first snippet can result in such code:
<ng-container *ngIf="hero$ | async as hero">
<p> {{ hero.name }} </p>
<p> {{ hero.surname }} </p>
<p> {{ hero.city }} </p>
</ng-container>
We are using ng-container
element here, so to not populate DOM with any unnecessary elements like <div>
elements (which could be tempting to use).
What about the other case, when we have a bunch of observables? We can combine them into a single Observable. This pattern is called View Model - since it is creating a model for use in the template. A common abbreviation for this is vm
, and that is often how we name the component’s property. For more information about the View Model pattern, I encourage you to read this inDepth article.
Combining Observables can be implemented using another type of RxJs operator. Pipeable operators are used to transforming Observable into another Observable, while Creation operators create new Observable and work as standalone functions.
We will use an operator called combineLatest
which merges several Observables and returns the result Observable, where each emitted value is an array of latest values from each source Observable. You can read more about this operator here.
Because the result Observable value will be an array, we will use the map
operator as well — to map the array into an object, so it will be more readable when using it in the template. Below is the fully implemented View Model for our case:
readonly vm$ = combineLatest([
this.route.params.pipe(
switchMap(params => this.heroService.getHero(params.id))
),
this.route.params.pipe(
switchMap(params => this.heroService.getPet(params.id))
),
this.heroService.getCities(),
]).pipe(
map(([hero, pet, cities]) => {
return {
hero,
pet,
cities
}
})
);
constructor(
private readonly route: ActivatedRoute,
private readonly heroService: HeroService,
) { }
It may look complex at glance, so let’s split this into two blocks:
readonly vm$ = combineLatest([
this.route.params.pipe(
switchMap(params => this.heroService.getHero(params.id))
),
this.route.params.pipe(
switchMap(params => this.heroService.getPet(params.id))
),
this.heroService.getCities(),
])
Which is the core, it uses the combineLatest
operator to create a vm$
Observable from three source Observables — hero, pet, and the cities.
The second block of code:
.pipe(
map(([hero, pet, cities]) => {
return {
hero,
pet,
cities
}
})
);
Is just about mapping the array into an object with readable property names.
Now let’s take a look at the template usage:
<ng-container *ngIf="vm$ | async as vm">
<p> {{ vm.hero.name }} </p>
<p> {{ vm.hero.surname }} </p>
<p> {{ vm.hero.city }} </p>
<p> {{ vm.pet.name }} </p>
<ul>
<li *ngFor="let city of vm.cities"> {{ city }} </li>
</ul>
</ng-container>
Everything is wrapped in the vm$
Observable, so once resolved via the | async
pipe our syntax becomes very simple — it simply operates on a regular object called vm
!
Initializtion that depends on the input values
Leaving the Observables and subscriptions behind for a moment, there are other cases when we use the ngOnInit
hook when we actually should use other hooks.
Components, especially the presentation ones are using input properties to get the state from the container/parent components. The components should react to the input data and display the result accordingly. Often when we start implementing a new component we tend to think about the input as a kind of setup information, which we use on components initialization and forget.
It can be the case, but often in the future when a component is used with input data that changes in time (for instance according to route params changes) it stops working correctly. If a component needs to run some computation using the input parameters, it’s often better to propose a code solution that will work every time, not only at the start.
Below is the example of a component’s code that although will work at the start, won’t update the data once input changes:
@Component({
template: `<p> {{ fullName }} </p>`,
})
export class NameComponent implements OnInit {
@Input() name: string;
@Input() surname: string;
fullName: string;
ngOnInit() {
this.fullName = `${this.name} ${this.surname}`;
}
}
Property fullname
will be initialized once with the first set of name
+ surname
, and later if the name
or surname
changes, the fullname
will stay the same.
In such cases, we should process input data every time it changes. How we can do this? There are two options:
- using a proper hook for that — so the `ngOnChanges`
- using a setters technique
The ngOnChanges
The ngOnChanges
runs on every input change, so we can use this hook to update our internal state according to the input data. It will work the same way as with the ngOnInit
, except it will also be triggered for any other changes in the future. The snippet below presents the idea:
@Component({
template: `<p> {{ fullName }} </p>`,
})
export class NameComponent implements OnChanges {
@Input() name: string;
@Input() surname: string;
fullName: string;
ngOnChanges() {
this.fullName = `${this.name} ${this.surname}`;
}
}
ngOnInit vs ngOnChanges
The ngOnInit
hook is being called just after the calling ngOnChanges
for the first time. So they are really similar, except that we can use ngOnChanges
to trigger any further updates.
The ngOnChanges
additionally gives us information about the current and previous state of the inputs via the SimpleChange
object.
We can say that although the ngOnInit
seems good for the initial setup, it is rarely useful because when the component gets updated input we quite often need to destroy the old instances and recreate them — which is why using the ngOnChanges
makes more sense.
Getters
The example from above is actually very simple, and most likely we will deal with far more advanced logic. Although if your case is as simple as creating a variable for a derived state, like joining two input arguments, then most likely you don’t even need to use the ngOnChanges
hook.
For such simple cases, you can use getters to provide the value when needed. Please make sure you are using ChangeDetection.OnPush
for that, to not get any performance issues if your getter runs some complicated logic behind.
The getters implementation looks as follows:
@Component({
template: `<p> {{ fullName }} </p>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NameComponent {
@Input() name: string;
@Input() surname: string;
get fullName(): string {
return `${this.name} ${this.surname}`
}
}
Setters
The third technique relies on the power of setters in TypeScript. There is no obligation that the Input decorator must be set on the property, and not the setter. That’s why we can use the setters, and put some light setup logic in there. Let me show you an example. Imagine we are receiving some data via input, which we need to somehow process. In our case, it will be again about taking the first element from the array and changing the string containing a date to a proper Date
object.
interface MyDTO {
data: {
name: string;
time: string;
}[]
}
@Component({
template: `<p> {{ time }} </p>`,
})
export class TimeComponent {
@Input()
set vm(value: MyDTO) {
const first = value.data[0];
this.time = new Date(first.time);
}
time: Date;
}
Using this trick we no longer need any lifecycle hooks used to set up components with proper data. We simply do the setup on data input. One of the advantages of this approach is, that we are limited to changes on a single property, while the ngOnChanges
is triggered each time any Input property is changed, even if we don’t want to do any setup on a particular property change.
Please note, that this technique works well when your result doesn’t depend on more than one input property. Using ngOnChanges
gives you the advantage of knowing that the change detection check has been completed and you can operate on the whole new state, while the setters can be run in the middle of the actual check.
Summary
This was an extensive article, but I hope it presents clearly some refactor ideas that could help make the component’s code more concise, and easier to understand and maintain.
In our components, we will deal with asynchronously loaded data quite often and we need to learn how to handle that so it is good in case of performance, contains no bugs, and is easier to read. Based on your preferences you may be a fan of the Promise approach or using Observables. If you use Observables, please remember that often there is no need for the subscriptions, nesting subscribes — and the only thing you need is a bit of knowledge about RxJs operators.
So what shouldn’t be a part of the ngOnInit
block?
- subscriptions when we can use the observables straightaway and subscribe in the templates
- any setup code that needs to be invoked on every input property change, not only at the component’s initial cycle
Finally when it probably makes the most sense to use the ngOnInit
?
- when we need to setup third-party components and libraries. They often need to provide them with HTML elements which we gather through static
ViewChild
declarations - when we need any additional logic to be done on component initialization, which cannot be done via Observables resolved in the template
- when we want to use Promise API
Although, to be honest, most of the time ngOnChanges
is probably a better alternative for ngOnInit
. It solves problems with reacting to changes while also giving the possibility to run some things only on the first call — same as ngOnInit
. It also gives us more flexibility, for instance when we want to destroy some third-party instances and recreate them with an updated state.
The special case occurs when the component has no inputs at all — in this situation, there is no point for ngOnChanges
and ngOnInit
is for sure the best hook to be used.
There is one more extra case that we need to consider when we use ngOnChanges
. Changing the component’s properties after change detection has been completed we will often end up with an Expression has changed after it was checked error. For that, we may need to schedule the work for the time after change detection finishes. We can schedule it by using requestAnimationFrame
, setTimeout
, or even a Promise
, to put the task on the macro or micro queue.
It seems, we really can live without auto-generated ngOnInit
in the default component generation in Angular CLI.