Angular Dependency Injection
Angular dependency injection system including providers, injection tokens, and hierarchical injectors
You are an expert in Angular's dependency injection system for building loosely coupled, testable Angular applications.
## Key Points
1. **Environment injectors** — root injector, module injectors, route-level injectors
2. **Element injectors** — component and directive injectors
1. **Default to `providedIn: 'root'` for application-wide singletons.** It is tree-shakable and requires no explicit provider registration.
2. **Use component-level providers for state that should be scoped.** Form state, editor state, and wizard state are good candidates for component-provided services.
3. **Prefer `inject()` over constructor parameters.** It is more concise, works in field initializers, and enables better refactoring.
4. **Use `InjectionToken` for configuration and non-class values.** Never use raw strings as tokens — they are not type-safe and collide easily.
5. **Design for testability.** Every service that depends on browser APIs (localStorage, fetch, window) should be behind an injectable abstraction so tests can substitute fakes.
6. **Avoid deep provider hierarchies.** If debugging "which instance am I getting?" becomes difficult, simplify the provider tree.
- **Circular dependencies.** Service A injects Service B which injects Service A. Angular throws a cyclic dependency error. Break the cycle by introducing a mediator service or using `forwardRef`.
- **Missing provider.** `NullInjectorError: No provider for X` means no injector in the chain has registered the dependency. Ensure the service is provided at the correct level.
- **Unintended singletons.** Providing a service at root when you need per-component instances means all components share the same state. Move the provider to the component level.
- **Forgetting `multi: true` on multi-providers.** Without `multi: true`, each provider registration replaces the previous one instead of appending.
## Quick Example
```typescript
private config = inject(APP_CONFIG);
```
```typescript
providers: [
{ provide: StorageService, useClass: LocalStorageService },
]
```skilldb get angular-skills/Angular Dependency InjectionFull skill: 284 linesDependency Injection — Angular
You are an expert in Angular's dependency injection system for building loosely coupled, testable Angular applications.
Core Philosophy
Angular's dependency injection system is the architectural backbone that makes the framework's components testable, configurable, and loosely coupled. The core idea is inversion of control: a component declares what it needs (a service, a configuration token, an abstract interface) without knowing how to create it. The injector resolves the dependency at runtime based on the provider configuration. This indirection is not overhead — it is the mechanism that lets you swap real services for test doubles, provide different implementations in different parts of the application, and tree-shake unused services.
The hierarchical injector system is Angular's most powerful and most misunderstood feature. Injectors form a tree: the root injector provides application-wide singletons, route-level injectors scope services to feature areas, and component-level injectors create per-instance state. Understanding which injector provides a given service determines its lifetime and sharing scope. A service provided at root is a singleton. The same service provided in a component's providers array creates a new instance for each component. This is not a configuration detail — it is an architectural decision about state ownership.
Modern Angular strongly favors the inject() function over constructor injection. It is more concise, works in field initializers and factory functions, and enables better refactoring. Combined with InjectionToken for non-class values and providedIn: 'root' for tree-shakable singletons, the modern DI API removes most of the ceremony that historically made Angular's DI feel heavy.
Anti-Patterns
-
Circular Service Dependencies — service A injecting service B which injects service A, causing Angular to throw a cyclic dependency error. Break the cycle with a mediator service, lazy injection via
Injector, orforwardRef. -
Accidental Multiple Instances via
useClass— writing{ provide: A, useClass: B }when B is alsoprovidedIn: 'root', creating two separate instances of B. UseuseExistingwhen you want to alias to the existing singleton. -
Raw String Tokens — using plain strings as injection tokens (
provide: 'apiUrl') instead ofInjectionToken<string>. String tokens are not type-safe, easily collide across libraries, and produce confusing runtime errors. -
Component-Level Providers for Intended Singletons — accidentally placing a service in a component's
providersarray when it should be a singleton, creating a new instance for every component instance and breaking shared state assumptions. -
Forgetting
multi: trueon Multi-Providers — registering multiple providers for the same token withoutmulti: true, causing each registration to silently replace the previous one instead of accumulating into an array.
Overview
Angular's dependency injection (DI) system is a design pattern and mechanism that allows classes to declare dependencies they need without creating them directly. Angular's DI is hierarchical — injectors form a tree that parallels the component tree, enabling precise control over service scope and lifetime.
Core Concepts
The inject() Function
The modern, preferred way to request dependencies in Angular:
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ActivatedRoute } from '@angular/router';
@Component({ /* ... */ })
export class ProductDetailComponent {
private http = inject(HttpClient);
private route = inject(ActivatedRoute);
}
inject() works in constructors, field initializers, and factory functions — anywhere an injection context is active.
providedIn: 'root'
The most common way to register a singleton service:
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class AuthService {
private token: string | null = null;
login(username: string, password: string) { /* ... */ }
logout() { this.token = null; }
isAuthenticated(): boolean { return this.token !== null; }
}
Services with providedIn: 'root' are tree-shakable — they are only included in the bundle if actually injected somewhere.
InjectionToken
Use InjectionToken for non-class dependencies (primitives, interfaces, configuration objects):
import { InjectionToken } from '@angular/core';
export interface AppConfig {
apiUrl: string;
enableDebug: boolean;
maxRetries: number;
}
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
Provide it during bootstrap:
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
{
provide: APP_CONFIG,
useValue: {
apiUrl: 'https://api.example.com',
enableDebug: false,
maxRetries: 3,
},
},
],
};
Inject it:
private config = inject(APP_CONFIG);
Hierarchical Injectors
Angular has two injector hierarchies:
- Environment injectors — root injector, module injectors, route-level injectors
- Element injectors — component and directive injectors
// Scoped to a component and its children
@Component({
selector: 'app-editor',
providers: [EditorStateService],
template: `...`,
})
export class EditorComponent {
private state = inject(EditorStateService);
}
Each EditorComponent instance gets its own EditorStateService. Child components that inject the same service receive the parent's instance.
Implementation Patterns
Factory Providers
Use useFactory when creation logic is complex or conditional:
export const appConfig: ApplicationConfig = {
providers: [
{
provide: LoggerService,
useFactory: () => {
const config = inject(APP_CONFIG);
return config.enableDebug
? new DebugLoggerService()
: new ProductionLoggerService();
},
},
],
};
Abstract Class as Token
Use an abstract class as both a type and a DI token — avoids the need for InjectionToken:
export abstract class StorageService {
abstract get(key: string): string | null;
abstract set(key: string, value: string): void;
abstract remove(key: string): void;
}
@Injectable()
export class LocalStorageService extends StorageService {
get(key: string) { return localStorage.getItem(key); }
set(key: string, value: string) { localStorage.setItem(key, value); }
remove(key: string) { localStorage.removeItem(key); }
}
providers: [
{ provide: StorageService, useClass: LocalStorageService },
]
Multi Providers
Register multiple values under the same token:
export const HTTP_INTERCEPTORS_TOKEN = new InjectionToken<HttpInterceptorFn[]>(
'http-interceptors'
);
providers: [
{ provide: VALIDATORS, useClass: RequiredValidator, multi: true },
{ provide: VALIDATORS, useClass: MinLengthValidator, multi: true },
{ provide: VALIDATORS, useClass: PatternValidator, multi: true },
]
Injecting VALIDATORS returns an array of all three validators.
Optional and Self/SkipSelf Decorators
Control resolution behavior:
import { inject, Optional, SkipSelf } from '@angular/core';
// Optional — returns null if not found
private analytics = inject(AnalyticsService, { optional: true });
// SkipSelf — skip the current injector, look only at ancestors
private parentState = inject(TreeNodeState, { skipSelf: true });
// Self — only look in the current injector
private localCache = inject(CacheService, { self: true });
Environment Injector for Dynamic Components
Create components dynamically with the correct injector:
import { Component, ViewContainerRef, EnvironmentInjector, inject, createEnvironmentInjector } from '@angular/core';
@Component({ /* ... */ })
export class DynamicHostComponent {
private vcr = inject(ViewContainerRef);
private envInjector = inject(EnvironmentInjector);
loadWidget(component: Type<unknown>) {
this.vcr.clear();
this.vcr.createComponent(component, {
environmentInjector: this.envInjector,
});
}
}
Functional Providers with makeEnvironmentProviders
Package a set of providers into a single callable function:
import { makeEnvironmentProviders, Provider } from '@angular/core';
export function provideFeatureFlags(flags: Record<string, boolean>) {
return makeEnvironmentProviders([
{ provide: FEATURE_FLAGS, useValue: flags },
FeatureFlagService,
]);
}
// app.config.ts
providers: [
provideFeatureFlags({ darkMode: true, betaSearch: false }),
]
Best Practices
-
Default to
providedIn: 'root'for application-wide singletons. It is tree-shakable and requires no explicit provider registration. -
Use component-level providers for state that should be scoped. Form state, editor state, and wizard state are good candidates for component-provided services.
-
Prefer
inject()over constructor parameters. It is more concise, works in field initializers, and enables better refactoring. -
Use
InjectionTokenfor configuration and non-class values. Never use raw strings as tokens — they are not type-safe and collide easily. -
Design for testability. Every service that depends on browser APIs (localStorage, fetch, window) should be behind an injectable abstraction so tests can substitute fakes.
-
Avoid deep provider hierarchies. If debugging "which instance am I getting?" becomes difficult, simplify the provider tree.
Common Pitfalls
-
Circular dependencies. Service A injects Service B which injects Service A. Angular throws a cyclic dependency error. Break the cycle by introducing a mediator service or using
forwardRef. -
Missing provider.
NullInjectorError: No provider for Xmeans no injector in the chain has registered the dependency. Ensure the service is provided at the correct level. -
Unintended singletons. Providing a service at root when you need per-component instances means all components share the same state. Move the provider to the component level.
-
Using
useClasswithout realizing it creates a new class.{ provide: A, useClass: B }creates a new instance of B. If B is alsoprovidedIn: 'root', you now have two instances. UseuseExistingif you want to alias to the existing singleton. -
Forgetting
multi: trueon multi-providers. Withoutmulti: true, each provider registration replaces the previous one instead of appending.
Install this skill directly: skilldb add angular-skills
Related Skills
Angular Testing
Testing Angular applications with Jest for unit tests and Cypress for end-to-end tests
Angular Forms
Reactive forms, form validation, dynamic forms, and typed form controls in Angular
Angular Ngrx
NgRx state management with store, effects, selectors, and the component store
Angular Routing
Angular Router configuration including lazy loading, guards, resolvers, and nested routes
Angular Rxjs Patterns
RxJS reactive patterns for data fetching, state management, and event handling in Angular
Angular Signals
Angular Signals for fine-grained reactivity and efficient change detection