Angular 17 constructor vs OnInit

2.7k views Asked by At

Related to but not the same as Difference between Constructor and ngOnInit.

In Angular 16 and 17 we can now use the takeUntilDestroyed operator and Signals. Both of these seems to work best in an Injector Context or at least has an advantage that you do not need to pass one.

Question

So the question is (again) should we put initialization in the constructor (or member fields) or still use OnInit? Second are there any pitfalls in using the constructor rather than OnInit?

Note: With initialization I mean using httpClient to fetch data to display on the page. Setup RxJS pipes with mapping of data, etc. Reading Route params, etc.

Set aside the following:

  • We need to use OnInit or OnChanges if we want to use @Input() variables
  • Personal preference

Additional information

According to the old Angular.io site's Component Lifecycle documentation:

Components should be cheap and safe to construct. You should not, for example, fetch data in a component constructor. You shouldn't worry that a new component will try to contact a remote server when created under test or before you decide to display it. An ngOnInit() is a good place for a component to fetch its initial data.

But this documentation does not exist in the new Angular.dev site.

Also one of their new tutorials has data calls in the constructor:

  constructor() {
    this.housingService.getAllHousingLocations().then((housingLocationList: HousingLocation[]) => {
      this.housingLocationList = housingLocationList;
      this.filteredLocationList = housingLocationList;
    });
  }

Summary

It seems Angular 16/17 is going in a direction where more initialization is done in an injection context (member field or constructor). Does that have implications on performance, stability, future development?

1

There are 1 answers

3
OZ_ On BEST ANSWER

It is better to initialize things in the member fields declaration. This way you can declare fields as readonly and you'll declare their type in the same line, and often it can be just inferred.

class ExampleComponent {
  private readonly userService = inject(UserService);
  private readonly users = this.userService.getUsers();
}

If some of the fields are initialized in the constructor (not in the list of arguments), then you'll need 2 lines - one to declare the field (and its type), and one to initialize it:

class ExampleComponent {
  private readonly users: Observable<User[]>;

  constructor(private readonly userService: UserService) {
    this.users = this.userService.getUsers();
  }
}

Also, if some of the fields are initialized in the constructor, you might have a situation when you use fields before they are initialized:

class ExampleComponent {
  private readonly users: Observable<User[]>;

  constructor(private readonly userService: UserService) {
    this.users = this.userService.getUsers();
  }

  private readonly emails = this.users.pipe(
    map((users) => users.map(user => user.email))
  );

}

No matter where emails is located, it will be initialized before the constructor(), and you'll get an error. And in reality code is not that short and simple, so it is quite easy to get this situation.

If fields are initialized in ngOnInit(), you will not be able to declare them as readonly, even if they can be readonly (and it's quite a useful safeguard).

ngOnInit() and ngOnChanges() are only needed if you need to read multiple Input() properties at once and execute logic, based on multiple of them. Although, I would recommend use setters and computed():

class ExampleComponent {
   private readonly $isReadonly = signal<boolean>(false);
   private readonly $hasEditPermissions = signal<boolean>(true);
   // 
   protected readonly $isEditButtonEnabled = computed(() => {
     return !this.$isReadonly() && this.$hasEditPermissions();
   });


   @Input() set isReadonly(isReadonly: boolean) {
     this.$isReadonly.set(isReadonly);
   }

   @Input() set hasEditPermissions(hasEditPermissions: boolean) {
     this.$hasEditPermissions.set(hasEditPermissions);
   }
}

With signal inputs, this code will be 6 lines shorter.

You shouldn't worry that a new component will try to contact a remote server when created under test or before you decide to display it

In tests, it is resolved with mocks. |async pipe and @defer resolve the second issue. Also, if you want to convert some observable into a signal, and don't want to subscribe until that part of the template is visible, there is a helper function in the NG Extension library: toLazySignal().