Advanced Dependency Injection in Angular
Dependency Injection (DI) is the architectural backbone of Angular. Since version 2,
the framework has offered a powerful and sophisticated DI system, but with Angular 14+ and the advent
of standalone components, the system has evolved radically. New APIs like inject(),
tree-shakable providers, functional guards and interceptors have transformed how we manage
dependencies in modern Angular applications.
In this ninth article of the Modern Angular series, we will explore advanced
Dependency Injection: from the new inject() function to custom tokens, from the
injector hierarchy to multi-provider patterns, all the way to testing and dynamic component
creation with injection context. If you already master the DI basics, this article will take
you to the next level.
What You Will Learn
- The
inject()function and why it replaces constructor injection - How to create and use typed
InjectionToken - The complete injector hierarchy: Root, Platform, Element, Component
- The
providedInoptions and their impact on the bundle - The
MULTIpattern for interceptors, guards, and multiple providers - Tree-shakable providers and bundle size optimization
- Functional guards and interceptors with
CanActivateFnandwithInterceptors() EnvironmentInjectorand dynamic component creationrunInInjectionContextfor utility functions withinject()- DI testing with
TestBedandoverrideProvider
Modern Angular Series Overview
| # | Article | Topic |
|---|---|---|
| 1 | Angular Signals | Fine-grained reactivity |
| 2 | Zoneless Change Detection | Removing Zone.js |
| 3 | New Template Syntax: @if, @for, @defer | Control flow blocks |
| 4 | Standalone Components | Module-free architecture |
| 5 | Signal Forms | Reactive forms with Signals |
| 6 | Angular SSR and Incremental Hydration | Server-side rendering |
| 7 | Core Web Vitals in Angular | Performance optimization |
| 8 | Angular PWA | Progressive Web Apps |
| 9 | You are here - Advanced Dependency Injection | DI patterns and tokens |
| 10 | Migrating from Angular 17 to 21 | Step-by-step migration |
1. The inject() Function: Goodbye Constructor
The inject() function, introduced as a stable API in Angular 14, represents the
most significant change in modern DI. Instead of declaring dependencies as constructor
parameters, you use a function that can be called wherever an active
injection context exists: in the constructor, in field initializers,
in factory functions, and in utility functions.
// BEFORE: Constructor injection (traditional approach)
@Component({
selector: 'app-user-list',
standalone: true,
template: '...'
})
export class UserListComponent {
constructor(
private userService: UserService,
private router: Router,
private route: ActivatedRoute,
@Inject(API_URL) private apiUrl: string,
@Optional() private logger?: LoggerService
) {}
}
// AFTER: inject() function (modern approach)
@Component({
selector: 'app-user-list',
standalone: true,
template: '...'
})
export class UserListComponent {
private userService = inject(UserService);
private router = inject(Router);
private route = inject(ActivatedRoute);
private apiUrl = inject(API_URL);
private logger = inject(LoggerService, { optional: true });
// Fields are already available, no constructor needed
users = this.userService.getUsers();
}
inject() Advantages Over Constructor Injection
| Aspect | Constructor | inject() |
|---|---|---|
| Readability | Long parameter lists, hard to manage | Inline declaration, clear and concise |
| Inheritance | Requires super() with all parameters |
No issues, fields are self-contained |
| Reusability | Only in Angular class constructors | Usable in functions, factories, utilities |
| Type safety | Complete with decorators | Complete, automatic type inference |
| Tree-shaking | Limited by decorator metadata | Optimal, direct class reference |
| Optional/Self/SkipSelf | Decorators: @Optional(), @Self() |
Options object: { optional: true, self: true } |
Where Can inject() Be Used
The inject() function works ONLY within an injection context.
Calling it outside this context throws a runtime error
NG0203: inject() must be called from an injection context.
- Allowed: component/service/directive/pipe constructors, field initializers, provider factory functions, functions executed inside
runInInjectionContext() - Not allowed: component methods called after initialization, event callbacks, setTimeout/setInterval, standalone functions without context
A particularly powerful pattern is using inject() to create
reusable utility functions that encapsulate injection logic:
// utils/navigation.ts
export function injectRouteParams() {
const route = inject(ActivatedRoute);
return toSignal(
route.paramMap.pipe(
map(params => ({
id: params.get('id'),
slug: params.get('slug')
}))
)
);
}
export function injectQueryParams() {
const route = inject(ActivatedRoute);
return toSignal(route.queryParamMap);
}
// Usage in component
@Component({ ... })
export class ProductDetailComponent {
params = injectRouteParams();
// params() returns { id: '123', slug: 'my-product' }
}
2. InjectionToken: Typed Tokens for Non-Class Values
When a dependency is not a class but a primitive value (string, number, configuration
object), Angular cannot use the class itself as a lookup key.
InjectionToken solves this problem by creating unique, typed tokens that
serve as keys in the DI registry.
import { InjectionToken, inject } from '@angular/core';
// Token with static value
export const API_BASE_URL = new InjectionToken<string>(
'API_BASE_URL'
);
// Token with factory function (tree-shakable!)
export const IS_MOBILE = new InjectionToken<boolean>(
'IS_MOBILE',
{
providedIn: 'root',
factory: () => {
const platform = inject(PLATFORM_ID);
if (isPlatformBrowser(platform)) {
return window.innerWidth < 768;
}
return false;
}
}
);
// Token for complex configuration
export interface AppConfig {
apiUrl: string;
production: boolean;
features: {
darkMode: boolean;
analytics: boolean;
};
}
export const APP_CONFIG = new InjectionToken<AppConfig>(
'APP_CONFIG',
{
providedIn: 'root',
factory: () => ({
apiUrl: 'https://api.example.com',
production: false,
features: {
darkMode: true,
analytics: true
}
})
}
);
// Usage in components
@Component({ ... })
export class AppComponent {
private config = inject(APP_CONFIG);
private isMobile = inject(IS_MOBILE);
}
When to Use InjectionToken
- Configuration values: API URLs, feature flags, timeouts, limits
- Browser APIs:
window,document,localStorage(for SSR compatibility) - Swappable implementations: logging strategy, storage adapter, authentication provider
- Dynamic factories: runtime-computed values that depend on other services
3. The Injector Hierarchy
One of the most powerful (and complex) aspects of Angular DI is its hierarchical structure. There is no single dependency container: Angular maintains a tree of injectors that mirrors the application structure. When a component requests a dependency, Angular walks up the tree until it finds one.
Angular Injector Hierarchy
+---------------------------------------------+
| Platform Injector |
| (shared across Angular applications) |
| Providers: PLATFORM_ID, PLATFORM_INITIALIZER|
+----------------------+----------------------+
|
+----------------------v----------------------+
| Root Injector |
| (providedIn: 'root', app.config.ts) |
| Providers: HttpClient, Router, root services|
+----------------------+----------------------+
|
+---------------+---------------+
| |
+------v------+ +-------v-----+
| Module | | Module |
| Injector | | Injector |
| (lazy mod.) | | (lazy mod.) |
+------+------+ +------+------+
| |
+------v------+ +-------v-----+
| Element | | Element |
| Injector | | Injector |
| (component) | | (component) |
+------+------+ +------+------+
| |
+------v------+ +-------v-----+
| Element | | Element |
| Injector | | Injector |
| (child) | | (child) |
+-------------+ +-------------+
Hierarchy Levels
| Level | Scope | Creation | Typical Use |
|---|---|---|---|
| Platform | All Angular apps on the page | platformBrowserDynamic() |
Services shared across micro-frontends |
| Root | Entire application (singleton) | providedIn: 'root' or app.config.ts |
HttpClient, Router, global services |
| Module | Lazy-loaded module | Module providers array |
Feature-specific services |
| Element | Component and its children | Component providers array |
Local state, multiple instances |
Dependency resolution follows a precise path: Angular starts from the injector of the
component requesting the dependency and walks up the tree until it finds a matching
provider. If no provider is found, a NullInjectorError is thrown
(unless the dependency is marked as optional).
// A service that manages local state
@Injectable()
export class FormStateService {
private fields = signal<Map<string, any>>(new Map());
private dirty = signal(false);
setValue(key: string, value: any) {
this.fields.update(m => {
const newMap = new Map(m);
newMap.set(key, value);
return newMap;
});
this.dirty.set(true);
}
isDirty = this.dirty.asReadonly();
}
// Each component instance gets its own FormStateService
@Component({
selector: 'app-editable-card',
standalone: true,
providers: [FormStateService], // LOCAL instance
template: `
<div class="card" [class.dirty]="formState.isDirty()">
<ng-content />
</div>
`
})
export class EditableCardComponent {
protected formState = inject(FormStateService);
}
4. The providedIn Options
The @Injectable() decorator accepts the providedIn option
that determines which injector the service is automatically registered in. This option
is fundamental for tree-shaking: Angular can remove unused services
from the final bundle.
Available providedIn Options
| Value | Scope | Tree-Shakable | Use Case |
|---|---|---|---|
'root' |
Singleton for the entire application | Yes | Global services (Auth, API, State) |
'platform' |
Shared across Angular applications | Yes | Micro-frontends, Elements |
'any' |
One instance per lazy-loaded module | Yes | Isolated state for lazy features |
| Component | One instance per component | No | Local state, forms, dialogs |
// Global singleton - the most common approach
@Injectable({ providedIn: 'root' })
export class AuthService {
private currentUser = signal<User | null>(null);
isAuthenticated = computed(() => this.currentUser() !== null);
}
// Shared across Angular apps on the same page
@Injectable({ providedIn: 'platform' })
export class SharedThemeService {
theme = signal<'light' | 'dark'>('dark');
}
// One instance per lazy-loaded module
@Injectable({ providedIn: 'any' })
export class FeatureAnalyticsService {
private events: AnalyticsEvent[] = [];
// Each lazy feature gets its own independent tracker
}
// Instance per component (not tree-shakable)
@Injectable() // No providedIn
export class DialogStateService {
isOpen = signal(false);
data = signal<any>(null);
}
// Must be registered in the component providers array:
@Component({
providers: [DialogStateService]
})
export class MyDialogComponent {
state = inject(DialogStateService);
}
Be Careful with providedIn: 'any'
The 'any' option creates a separate instance for each lazy-loaded module.
This can cause unexpected behavior if you expect a singleton. Use it only when you
explicitly want to isolate state between feature modules. With standalone architecture
and lazy routes, 'any' is rarely needed: prefer 'root' for
singletons or component providers for local state.
5. Multi Providers: The MULTI Pattern
The multi-provider pattern allows associating multiple values with the same injection token. When the token is requested, Angular returns an array of all registered values. This pattern underpins critical features such as HTTP interceptors, route guards, and application initializers.
import { InjectionToken, inject } from '@angular/core';
// Multi-provider token for custom validators
export const FORM_VALIDATORS = new InjectionToken<FormValidator[]>(
'FORM_VALIDATORS'
);
export interface FormValidator {
name: string;
validate(value: any): string | null;
}
// Registering multiple providers with multi: true
export const appConfig: ApplicationConfig = {
providers: [
{
provide: FORM_VALIDATORS,
useValue: {
name: 'required',
validate: (v: any) => v ? null : 'Field is required'
},
multi: true
},
{
provide: FORM_VALIDATORS,
useValue: {
name: 'minLength',
validate: (v: string) =>
v?.length >= 3 ? null : 'Minimum 3 characters'
},
multi: true
},
{
provide: FORM_VALIDATORS,
useFactory: () => {
const http = inject(HttpClient);
return {
name: 'uniqueEmail',
validate: async (v: string) => {
// Async validation via API
return null;
}
};
},
multi: true
}
]
};
// Consuming in a service
@Injectable({ providedIn: 'root' })
export class ValidationService {
private validators = inject(FORM_VALIDATORS);
validateAll(value: any): string[] {
return this.validators
.map(v => v.validate(value))
.filter((err): err is string => err !== null);
}
}
Built-in Multi-Providers in Angular
HTTP_INTERCEPTORS- HTTP interceptors (legacy class-based approach)APP_INITIALIZER- Functions executed before app bootstrapENVIRONMENT_INITIALIZER- Initializers for environment injectorsNG_VALIDATORS/NG_ASYNC_VALIDATORS- Form validatorsROUTES- Dynamic route configuration
6. Tree-Shakable Providers
One of the main advantages of modern Angular DI is support for
tree-shaking providers. When a service is registered with
providedIn: 'root' and a factory in the decorator, the bundler can
completely remove it from the final build if no component imports it.
// TREE-SHAKABLE: the service self-registers
// If no component/service imports it, it is removed from the bundle
@Injectable({ providedIn: 'root' })
export class AnalyticsService {
track(event: string) {
// If AnalyticsService is not imported anywhere,
// all this code is removed from the bundle
console.log('Track:', event);
}
}
// TREE-SHAKABLE with factory: computed dependencies
export const FEATURE_FLAGS = new InjectionToken<FeatureFlags>(
'FEATURE_FLAGS',
{
providedIn: 'root',
factory: () => {
const http = inject(HttpClient);
// The factory is removed if FEATURE_FLAGS
// is never injected
return { darkMode: true, newDashboard: false };
}
}
);
// NOT TREE-SHAKABLE: registered in the providers array
// Always included in the bundle, even if unused
export const appConfig: ApplicationConfig = {
providers: [
LegacyService, // Always in the bundle!
{ provide: LoggerService, useClass: ConsoleLoggerService }
]
};
Tree-Shaking Impact on Bundle Size
| Approach | Registration | Tree-Shakable | In Bundle If Unused |
|---|---|---|---|
providedIn: 'root' |
In the service decorator | Yes | No (removed) |
InjectionToken with factory |
In the token constructor | Yes | No (removed) |
providers array |
In app.config.ts or component | No | Yes (always present) |
NgModule providers |
In the module | No | Yes (always present) |
Tree-Shaking Best Practices
- Always use
providedIn: 'root'for global services - Define factories in the tokens themselves, not in the providers array
- Avoid registering services in the
providersarray ofapp.config.tsunless necessary (overrides, multi-providers) - Monitor bundle size with
ng build --stats-jsonandwebpack-bundle-analyzer
7. Functional Guards and Interceptors
Starting with Angular 15, guards and interceptors have evolved from classes with interfaces
to simple functions. This functional approach reduces boilerplate,
improves composability, and natively leverages the inject() function.
7.1 Functional Guards
// BEFORE: Guard as class (deprecated)
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(
private auth: AuthService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean | UrlTree {
if (this.auth.isAuthenticated()) {
return true;
}
return this.router.createUrlTree(['/login']);
}
}
// AFTER: Guard as function
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isAuthenticated()) {
return true;
}
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url }
});
};
// Guard with parameters: factory function
export function roleGuard(requiredRole: string): CanActivateFn {
return (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.hasRole(requiredRole)) {
return true;
}
return router.createUrlTree(['/forbidden']);
};
}
// Route configuration
export const routes: Routes = [
{
path: 'dashboard',
canActivate: [authGuard],
loadComponent: () => import('./dashboard/dashboard.component')
.then(m => m.DashboardComponent)
},
{
path: 'admin',
canActivate: [authGuard, roleGuard('admin')],
loadComponent: () => import('./admin/admin.component')
.then(m => m.AdminComponent)
}
];
7.2 Functional Interceptors
import { HttpInterceptorFn } from '@angular/common/http';
// Authentication interceptor
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
const token = auth.getToken();
if (token) {
const cloned = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next(cloned);
}
return next(req);
};
// Logging interceptor
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
const startTime = Date.now();
return next(req).pipe(
tap(event => {
if (event instanceof HttpResponse) {
const duration = Date.now() - startTime;
console.log(`${req.method} ${req.url} - ${duration}ms`);
}
})
);
};
// Retry interceptor with backoff
export const retryInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(
retry({
count: 3,
delay: (error, retryCount) => {
if (error.status === 429 || error.status >= 500) {
return timer(Math.pow(2, retryCount) * 1000);
}
throw error;
}
})
);
};
// Registration in app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([
authInterceptor,
loggingInterceptor,
retryInterceptor
])
)
]
};
Class-based vs Functional Interceptors
| Aspect | Class-based (legacy) | Functional (modern) |
|---|---|---|
| Syntax | Class + HttpInterceptor interface |
Simple HttpInterceptorFn function |
| Registration | HTTP_INTERCEPTORS multi-provider |
withInterceptors([]) |
| DI | Constructor | inject() |
| Composition | Hard to parameterize | Factory functions for parameters |
| Tree-shaking | Limited | Optimal |
8. EnvironmentInjector and Dynamic Components
EnvironmentInjector is a type of injector introduced in Angular 14 that
operates at the "environment" level (root, route, lazy module) rather than at the DOM
element level. It is essential when creating components dynamically with
createComponent() or ViewContainerRef, as it allows
dynamically created components to access the correct injection context.
import {
Component,
EnvironmentInjector,
createEnvironmentInjector,
createComponent,
ViewContainerRef,
inject
} from '@angular/core';
// Service for creating components with a custom DI context
@Injectable({ providedIn: 'root' })
export class DynamicComponentService {
private parentInjector = inject(EnvironmentInjector);
createIsolatedComponent<T>(
component: Type<T>,
vcr: ViewContainerRef,
providers: StaticProvider[] = []
) {
// Create a new EnvironmentInjector with additional providers
const childInjector = createEnvironmentInjector(
[
{ provide: DIALOG_DATA, useValue: { title: 'My Dialog' } },
...providers
],
this.parentInjector
);
// Create the component in the new context
const componentRef = createComponent(component, {
environmentInjector: childInjector,
hostElement: document.createElement('div')
});
// Insert into the view
vcr.insert(componentRef.hostView);
// Important: destroy the injector when the component is destroyed
componentRef.onDestroy(() => childInjector.destroy());
return componentRef;
}
}
// Usage in a host component
@Component({
selector: 'app-dialog-host',
standalone: true,
template: `<ng-container #outlet />`
})
export class DialogHostComponent {
private dynamicService = inject(DynamicComponentService);
private vcr = inject(ViewContainerRef);
openDialog(dialogComponent: Type<any>, data: any) {
this.dynamicService.createIsolatedComponent(
dialogComponent,
this.vcr,
[{ provide: DIALOG_DATA, useValue: data }]
);
}
}
When to Use EnvironmentInjector
- Dynamic dialogs/modals: components created at runtime that need injected data
- Plugin system: dynamic loading of components with isolated providers
- Micro-frontends: each micro-app gets its own injection context
- Advanced testing: creating isolated injection contexts for tests
9. runInInjectionContext: Injection Anywhere
The runInInjectionContext() function, introduced in Angular 16, allows
running arbitrary code within an injection context. This solves one of the limitations
of inject(): the ability to call it only during the construction phase.
With runInInjectionContext(), any function can access dependencies.
import {
runInInjectionContext,
EnvironmentInjector,
inject,
DestroyRef
} from '@angular/core';
// Utility function that requires inject()
function createAutoSaveEffect(formId: string) {
const http = inject(HttpClient);
const destroyRef = inject(DestroyRef);
const intervalId = setInterval(() => {
const formData = getFormData(formId);
http.post('/api/autosave', formData).subscribe();
}, 30000);
// Automatic cleanup when the context is destroyed
destroyRef.onDestroy(() => clearInterval(intervalId));
}
// Usage in component: works in constructor (active context)
@Component({ ... })
export class EditorComponent {
constructor() {
createAutoSaveEffect('editor-form');
}
}
// Usage outside constructor: runInInjectionContext
@Component({ ... })
export class DashboardComponent {
private envInjector = inject(EnvironmentInjector);
onTabSelected(tabId: string) {
// This method is called from a click handler,
// so inject() would not work directly
runInInjectionContext(this.envInjector, () => {
createAutoSaveEffect(tabId);
});
}
}
// Advanced pattern: create a function-based "service locator"
export function createServiceAccessor<T>(token: ProviderToken<T>) {
let cachedInstance: T | undefined;
return {
init: () => {
cachedInstance = inject(token);
},
get: () => {
if (!cachedInstance) {
throw new Error('Service not initialized. Call init() first.');
}
return cachedInstance;
}
};
}
Be Careful with runInInjectionContext
While powerful, runInInjectionContext() should not be overused.
Use it when you have a concrete reason to run code with injection outside
the construction phase. If you find yourself using it frequently, the component
design can likely be improved. Also remember that the injector passed must be an
EnvironmentInjector (not an ElementInjector),
so providers registered at the component level will not be accessible.
10. Testing Dependency Injection
One of the greatest advantages of DI is testability. Angular provides powerful tools for
configuring, replacing, and verifying dependencies in unit tests. With the modern approach
based on inject() and standalone components, test setup has become
significantly simpler.
import { TestBed } from '@angular/core/testing';
describe('UserService', () => {
let service: UserService;
let httpMock: jest.Mocked<HttpClient>;
beforeEach(() => {
// Mock the HttpClient
const mockHttp = {
get: jest.fn(),
post: jest.fn()
};
TestBed.configureTestingModule({
providers: [
UserService,
{ provide: HttpClient, useValue: mockHttp },
{ provide: API_BASE_URL, useValue: 'http://test-api.com' }
]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpClient) as jest.Mocked<HttpClient>;
});
it('should fetch users from the API', () => {
const mockUsers = [{ id: 1, name: 'Alice' }];
httpMock.get.mockReturnValue(of(mockUsers));
service.getUsers().subscribe(users => {
expect(users).toEqual(mockUsers);
expect(httpMock.get).toHaveBeenCalledWith(
'http://test-api.com/users'
);
});
});
});
// Testing a standalone component with overrides
describe('DashboardComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DashboardComponent],
})
.overrideComponent(DashboardComponent, {
set: {
providers: [
{
provide: AnalyticsService,
useValue: { track: jest.fn() }
}
]
}
})
.compileComponents();
});
it('should track page view on init', () => {
const fixture = TestBed.createComponent(DashboardComponent);
const analytics = fixture.debugElement.injector.get(AnalyticsService);
fixture.detectChanges();
expect(analytics.track).toHaveBeenCalledWith('dashboard_view');
});
});
// Testing functional guards
describe('authGuard', () => {
it('should redirect to login when not authenticated', () => {
TestBed.configureTestingModule({
providers: [
{
provide: AuthService,
useValue: { isAuthenticated: () => false }
},
provideRouter([])
]
});
const injector = TestBed.inject(EnvironmentInjector);
// Run the guard in the injection context
const result = runInInjectionContext(injector, () =>
authGuard(
{} as ActivatedRouteSnapshot,
{ url: '/dashboard' } as RouterStateSnapshot
)
);
expect(result).toBeInstanceOf(UrlTree);
});
it('should allow access when authenticated', () => {
TestBed.configureTestingModule({
providers: [
{
provide: AuthService,
useValue: { isAuthenticated: () => true }
},
provideRouter([])
]
});
const injector = TestBed.inject(EnvironmentInjector);
const result = runInInjectionContext(injector, () =>
authGuard(
{} as ActivatedRouteSnapshot,
{ url: '/dashboard' } as RouterStateSnapshot
)
);
expect(result).toBe(true);
});
});
DI Testing Patterns
| Scenario | Method | Example |
|---|---|---|
| Replace a service | provide + useValue |
{ provide: AuthService, useValue: mockAuth } |
| Component-level override | overrideComponent |
Component local providers |
| Test InjectionToken | provide + useValue |
{ provide: API_URL, useValue: 'http://test' } |
| Test functional guards | runInInjectionContext |
Run the guard in TestBed context |
| Spy on real methods | inject + spyOn |
spyOn(TestBed.inject(Svc), 'method') |
DI Testing Best Practices
- Prefer manual mocks over
jasmine.createSpyObjfor better type control - Test services in isolation: mock all dependencies, test only the service logic
- Use
TestBed.inject()to access services registered in the testing module - For functional guards and interceptors:
runInInjectionContext()with the TestBed injector - Verify errors: test that
NullInjectorErroris thrown when required providers are missing
Summary and Next Steps
In this article we explored advanced Dependency Injection in Angular, from the new
inject() API all the way to testing. Modern Angular DI is much more than
a simple object creation mechanism: it is a sophisticated system that influences the
architecture, performance, and maintainability of your application.
Key Takeaways
- The
inject()function replaces constructor injection, offering better readability, reusability, and tree-shaking InjectionTokenenables injecting non-class values with strong typing and tree-shakable factories- The injector hierarchy (Platform, Root, Module, Element) determines the scope and lifetime of dependencies
providedIn: 'root'is the preferred option for tree-shakable singleton services- The
multi: truepattern underpins interceptors, guards, and initializers - Functional guards (
CanActivateFn) and functional interceptors (HttpInterceptorFn) reduce boilerplate and improve composability EnvironmentInjectorandcreateEnvironmentInjector()enable dynamic component creation with isolated DI contextsrunInInjectionContext()allows usinginject()outside the construction phase- DI testing leverages
TestBed,overrideComponent, andrunInInjectionContextfor comprehensive mocking and verification
In the next and final article of the series, Migrating from Angular 17 to 21, we will see how to apply all the knowledge gained in this series to migrate an existing application to Angular's modern APIs, with a step-by-step guide covering every aspect of the transition.
Related deep dive: 7 - Ottimizzare Core Web Vitals in Angular.
Related deep dive: 8 - Angular + PWA: Creare un\.
Frequently asked questions
What is the main change introduced by the inject() function in modern Angular Dependency Injection?
The inject() function allows declaring dependencies as a function that can be called within an injection context, replacing the need to declare them as constructor parameters.
How does Angular resolve dependencies when they are not classes but primitive values such as strings or numbers?
Angular uses typed tokens created with InjectionToken to serve as keys in the DI registry for resolving non-class dependencies.
What is the purpose of the providedIn options in the @Injectable decorator, and how does it affect tree-shaking?
The providedIn option determines which injector a service is automatically registered in, allowing Angular to remove unused services from the final bundle through tree-shaking.
What is the multi-provider pattern, and what are its benefits in modern Angular applications?
The multi-provider pattern allows associating multiple values with the same injection token, returning an array of all registered values when requested, which underpins features like HTTP interceptors and route guards.
Commenti
Caricamento commenti...
Sign in to comment