{"community_link":"https://github.com/indepth-dev/community/discussions/289"}

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.

Component initialization without ngOnInit with async pipes for Observables and 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 for ngOnInit

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.

  1. component’s property data$ has a value from the start, the Observable is assigned at constructor time. The property is no longer optional.
  2. 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.
  3. there is no subscription (in the component’s class), so no need to unsubscribe, and the ngOnDestroy hook
  4. 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:

  1. using a proper hook for that — so the `ngOnChanges`
  2. 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.