![]()
Dependency Injection (DI) is a fundamental design pattern in Angular that helps manage dependencies in a structured and efficient way. It allows components, services, and modules to request instances of dependencies rather than creating them manually.
With each new Angular release, there are improvements and changes in DI, making it essential for developers to adapt their applications accordingly.
In this guide, we will cover:
- Basics of DI in Angular
- Changes in Angular DI across versions
- Step-by-step approach to handling DI changes
- Best practices and troubleshooting
1. Basics of Dependency Injection in Angular
1.1 What is Dependency Injection?
Dependency Injection (DI) is a design pattern used to inject dependencies (services, objects, or values) into components and other services rather than creating them inside a class. It follows Inversion of Control (IoC), where dependencies are provided externally.
1.2 DI Components in Angular
Angular’s DI system consists of:
- Providers – Define how a dependency is created.
- Injectors – Responsible for injecting dependencies.
- Tokens – Unique identifiers for dependencies.
1.3 Registering Dependencies in Angular
Dependencies in Angular are registered using:
- In a Module (
providersarray in@NgModule) - In a Component (
providersarray in@Component) - In a Service (
providedInin@Injectable)
Example of DI in a service:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root', // Registers at the root level
})
export class ExampleService {
constructor() { }
}
2. Changes in Angular Dependency Injection Over Versions
2.1 ProvidedIn Property (Angular 6+)
Before Angular 6, services were registered in the providers array inside a module. With Angular 6+, the providedIn property was introduced inside @Injectable(), allowing tree-shakable providers.
Old Approach (Before Angular 6)
@NgModule({
providers: [ExampleService]
})
export class AppModule { }
New Approach (Angular 6+)
@Injectable({
providedIn: 'root'
})
export class ExampleService { }
2.2 Tree-shakable Providers (Angular 9+)
Angular 9 introduced tree-shakable providers, ensuring that unused services are removed from production builds.
Example:
@Injectable({
providedIn: 'any' // Creates a new instance for each module
})
export class ExampleService { }
2.3 Injecting Dependencies in Functional Guards (Angular 14+)
With Angular 14, functional route guards (canActivate, canMatch, etc.) use DI in a different way.
Old Approach (Class-Based Guards):
@Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService) {}
canActivate(): boolean {
return this.authService.isAuthenticated();
}
}
New Approach (Functional Guards in Angular 14+):
export const authGuard: CanActivateFn = (route, state) => {
return inject(AuthService).isAuthenticated();
};
3. Step-by-Step Guide to Handling DI Changes in Angular
Step 1: Identifying Dependency Injection Changes
Whenever Angular releases a new version, check for DI-related updates in the official documentation. Look for changes related to:
providedInproperty- New DI tokens
- Functional guards and resolvers
- Injection techniques in standalone components
Step 2: Updating providedIn Usage
If you are still registering services in the providers array of a module, update them using providedIn: 'root' or providedIn: 'any'.
Before (Old Approach)
@NgModule({
providers: [LoggingService]
})
export class AppModule { }
After (Updated Approach)
@Injectable({
providedIn: 'root'
})
export class LoggingService { }
Step 3: Adapting to Functional Guards (Angular 14+)
If you are using old class-based guards, migrate them to functional guards.
Before (Class-Based Guard)
@Injectable({
providedIn: 'root'
})
export class RoleGuard implements CanActivate {
constructor(private authService: AuthService) {}
canActivate(): boolean {
return this.authService.hasRole('admin');
}
}
After (Functional Guard)
export const roleGuard: CanActivateFn = (route, state) => {
return inject(AuthService).hasRole('admin');
};
Step 4: Using Injection Tokens for Flexibility
Angular allows creating custom tokens to provide dependencies dynamically. If you have hardcoded values in DI, replace them with Injection Tokens.
Before (Hardcoded Value in Provider)
@NgModule({
providers: [
{ provide: 'API_URL', useValue: 'https://api.example.com' }
]
})
export class AppModule { }
After (Using Injection Token)
import { InjectionToken } from '@angular/core';
export const API_URL = new InjectionToken<string>('API_URL');
@NgModule({
providers: [
{ provide: API_URL, useValue: 'https://api.example.com' }
]
})
export class AppModule { }
Step 5: Migrating to Standalone Components (Angular 15+)
Angular 15 introduced standalone components, eliminating the need for NgModules. If you are migrating to standalone components, handle DI properly.
Before (Using NgModule)
@NgModule({
declarations: [HomeComponent],
imports: [CommonModule],
providers: [DataService]
})
export class HomeModule { }
After (Standalone Component Approach)
@Component({
selector: 'app-home',
standalone: true,
templateUrl: './home.component.html',
providers: [DataService],
imports: [CommonModule]
})
export class HomeComponent { }
