The constructor
and ngOnInit
are both available lifecycle hooks when developing Angular applications. Both provide specific benefits, and understanding the difference allows you to develop predictable, extendable, and testable applications. Often the constructor
ends up doing the majority of work during class initialization, However, ngOnInit
may be better suited depending on your task.
Angular applications rely heavily on classes. When writing a component, you’ll typically find a constructor
declared inside a class:
export class AppComponent { constructor() {} }
MDN defines the constructor
as:
a special method for creating and initializing an object created with a class. There can only be one special method with the name “constructor” in a class.
The constructor
has a particular purpose inside an Angular application with unique features provided by the ECMAScript 2015 specification, Typescript, and Angular itself. Let’s look at each in more detail.
ECMAScript 2015 constructor
Javascript classes were introduced in ECMAScript 2015 and allow you to initialize and customize a class’s instantiation by utilizing the constructor
. In essence, you can construct new classes and control that construction with the constructor
method. By defining parameters in the constructor
, you can control how you instantiate or “new up” a class by passing different arguments similar to normal functions.
Let’s assume we have a Logger that logs messages to the UI and the server:
class Logger { constructor(format) { this.fmt = format; } send(options) { this.fmt.send(options); } } // for logging to the server const serverLogger = new Logger(ServerFormat); // for logging to the front end const uiLogger = new Logger(UIFormat);
The constructor
has two main jobs in this example:
- Setting a member property (
this.fmt
) inside theconstructor
. This block executes as soon as the class instantiates. - Accept arguments to control how messages are formatted when creating a new Logger class.
Typescript constructor
Typescript adds some syntactic sugar on top of the ECMAScript specification. Everything above is still valid when using Typescript and constructors, However, you can do more with less code. When defining parameters in the constructor
, we can leverage parameter properties to create and initialize a member in one place.
// without parameter properties class Logger { private fmt; constructor(format) { this.fmt = format; } } // with parameter properties class Logger { constructor(private fmt) {} send(options) { this.fmt.send(options); } }
By declaring the constructor
parameter with either public, private, or protected it creates and initializes the member in place, allowing you to access this.fmt
inside the class, just like the previous example.
Angular constructor
Angular applications build on the concept of dependency injection. When a class is initialized and requests an injectable resource, dependency injection finds the resource and provides it to the requestor. During the lifetime of an application this process may happen once or many times, depending on how the class is consumed.
Angular leverages all the benefits listed so far and takes it one step further. By declaring a constructor
parameter with an injectable type, Angular can search the dependency tree and find the closest resource. This resolution is possible because of dependency injection, modules, and decorators. Using our previous example with Angular looks like this:
import { FormatService } from "./format.service"; @Injectable({ ... }) export class Logger { constructor(private fmt: FormatService) {} send(options) { this.fmt.send(options); } }
Here we combine everything mentioned so far:
- Accept an argument to control how the Logger class functions regarding the formatting.
- Create and initialize the member property (
this.fmt
) in theconstructor
- We leverage dependency injection to control which “FormatService” class the Logger class receives.
ngOnInit
The features available in an Angular class constructor
are powerful yet concise, However, there’s still more control and benefits available in the ngOnInit
lifecycle method.
Angular lifecycle hooks
Components and directives start their lifecycle when Angular instantiates the class and renders any relevant views. It continues in sequence through change detection and ends when the component or directive is destroyed.
ngOnInit vs constructor
The ngOnInit
lifecycle hook should contain complex initialization, such as fetching data or accessing other classes. Testing is made more accessible by moving initialization logic into a method you can call when needed instead of every class initialization. If you require access to inputs, it’s important to know that data-bound properties might not be available until the ngOnInit
hook. For example, directives do not have their data-bound properties set until after construction, making ngOnInit
the appropriate location to initialize and interact with data-bound properties. These reasons make ngOnInit
useful for:
- Initialization (complex or not)
- Fetching data
- The first interaction with data-bound properties
The constructor
should be used to manage dependencies and set initial member variables to simple values. Managing dependencies through dependency injection in the constructor
provides more benefits rather than a manual approach. If a constructor
were to manually “new up” a class, it would be violating the Single Responsibility Principle and close off any reuse opportunities that might otherwise be available for that class. These reasons make the constructor
useful for:
- Requesting dependencies through dependency injection
- Creating and initializing members in one place with parameter properties
- Set initial member variables to simple values
Below is a typical example of using both the constructor
and ngOnInit
while maintaining a predictable and testable component.
@Component({ ... }) export class AppComponent { orders$; constructor(private orderService: OrderService) { } ngOnInit() { this.orders$ = this.orderService.getOrders(); } }
Summary
Hopefully, it’s clear where each has its strengths. The constructor
should favor simple tasks, and dependency injection while ngOnInit
should handle complex tasks, initialization, and data-bound properties.
When deciding between these two, it’s helpful to ask how easy is this to test? If complex initialization logic exists in the constructor
, then you force every test and collaborator of that class to own the initialization burden and reduce reuse opportunities.
If you can remove the difficulty of constructing the class in isolation, testing is simplified and predictable. By utilizing ngOnInit
, you’ll be able to control when initialization occurs during testing and only introduce test setup cost when necessary.