8

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 Answer 1

12

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().

Sign up to request clarification or add additional context in comments.

3 Comments

I agree with everything you write, but it is more how to do it and not why. The linked stack overflow question has 1500 votes for using OnInit: "So you should use constructor() to setup Dependency Injection and not much else". This combined with Angular's documentation makes me hesitate to use constructor/properties (which is basically the same in this context). What I'm looking for is reassurance that IF we take our project in this direction, we will not run into complex problems later with performance, rendering, dynamic component creation, etc.
Also are the old Angular documentation wrong and the old StackOverflow answer? Or has something changed that mitigates the concerns (code wise or design pattern wise)?
ngOnInit() is executed outside the injection context, so you can not even declare dependencies there (or you'll need to inject an Injector before). Old Angular documentation is just old - we all evolve and learn, and Angular has evolved significantly in the last few years. inject() has changed things drastically and opened a lot of new opportunities. @defer brought a whole new mindset. Currently, you don't need any lifecycle hook to create Angular components. They still exist only for compatibility reasons.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.