One of the key elements of Angular is the Dependency Injection (DI) mechanism, which makes managing dependencies in an application easy. Since the framework’s inception, dependency injection has been an integral part of Angular’s philosophy. However, as the ecosystem evolved, the DI tools had to be adapted to new developer needs. In this context, the inject() method was introduced.
For an in-depth explanation of how DI works in Angular, check out our comprehensive article on the subject.
Historical Context of Dependency Injection in Angular
For many years, dependency injection in Angular was implemented through class constructors. Components, services, and directives could define their dependencies in the constructor, and Angular would automatically provide the appropriate instances when creating objects. This approach worked well but also introduced some limitations and challenges:
- Complex Configuration: As large applications grew, the number of dependencies could increase rapidly, leading to complicated constructor configurations.
- Testing: Constructor-based injection could be problematic during testing, especially when mocking dependencies.
- Functions vs. Classes: There were scenarios in Angular where developers wanted to use DI in the context of functions rather than classes. The constructor-based approach did not support DI outside the class context.
To address these issues, Angular 14 introduced a new method called inject()
. This method allows access to dependencies anywhere in the code, not just within class constructors, opening new possibilities for developers.
Simplifying Inheritance – inject() is coming
One of the challenges developers faced in Angular before the introduction of the inject()
method was inheriting classes for services, components, or other units with dependencies. In the traditional DI approach, when a class inherited from another class, all dependencies had to be passed to the parent constructor using the super()
function. This could be cumbersome, especially in large applications where the number of dependencies grew quickly.
Traditional Inheritance with Constructors
In a typical Angular implementation before inject()
was introduced, if class B inherited from class A, and both classes had their own dependencies, the dependencies needed to be explicitly passed to the base class constructor.
Example:
class A {
constructor(private serviceA: ServiceA, private serviceB: ServiceB) {}
}
class B extends A {
constructor(serviceA: ServiceA, serviceB: ServiceB, private serviceC: ServiceC) {
super(serviceA, serviceB); // Dependencies must be manually passed
}
}
As shown, for class B to correctly initialize class A, the developer had to manually pass all dependencies to the super()
method. As the number of dependencies grew, the code became increasingly complex and error-prone.
Using inject()
class A {
private serviceA = inject(ServiceA);
private serviceB = inject(ServiceB);
}
class B extends A {
private serviceC = inject(ServiceC);
}
In this approach:
Class A uses the inject()
method to inject its dependencies (ServiceA and ServiceB).
Class B inherits from class A but no longer needs to pass dependencies to the parent constructor. Instead, it uses the inject()
method to inject its own dependencies (ServiceC).
Creating Reusable Utility Methods
With inject()
, we can define reusable functions that perform operations on the router, store, or signals, which can be called anywhere in the application without needing to write additional code.
Example (function retrieving the page
parameter from ActivatedRoute
):
const getPageParam = () => {
return inject(ActivatedRoute).queryParams.pipe(
filter((params) => params['page']),
map((params) => params['page'])
);
}
@Component()
export class AppComponent {
page$ = getPageParam();
}
Injecting Dependencies into Guards – inject() again
In Angular version 16 (and later), there were significant changes to how guards are created, and interfaces such as CanActivate
, CanActivateChild
, CanDeactivate
, CanLoad
, and others related to route protection have been marked as deprecated. They have been replaced by new functions based on observables, signals, and functions.
Angular is shifting towards a more reactive approach to managing route protection. Version 16 introduced a new way of defining guards that eliminates the need to implement interfaces. With this change, it is no longer possible to inject dependencies into them through the constructor.
Example implementation:
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard = () => {
const authService = inject(AuthService); // Inject AuthService using inject()
const router = inject(Router); // Inject Router
return authService.isAuthenticated() ? true : router.parseUrl('/login');
};
Summary
The inject()
method in Angular introduces a new way of managing dependencies, which significantly simplifies inheritance. This allows developers to focus on application logic rather than worrying about manually passing dependencies to base class constructors. It also opens up new possibilities for creating reusable utility functions that can operate on various dependencies such as navigation, state management, and signals. The method for creating guards has also changed. Although traditional constructor injection is still available, I highly recommend using inject()
exclusively. Mixing both solutions within a single application can lead to considerable confusion, and the benefits of using inject()
are substantial, making it worth migrating across the entire application.