Injection de Dépendances Angular : Guide Avancé
Le système d'injection de dépendances d'Angular est bien plus qu'un simple conteneur de services. C'est un système hiérarchique sophistiqué qui contrôle le cycle de vie des instances, leur portée, et leur résolution. Ce guide explore les mécanismes internes et les patterns avancés que vous rencontrerez dans des applications de production.
Hiérarchie des Injecteurs : Comprendre la Résolution
Angular maintient un arbre d'injecteurs parallèle à l'arbre des composants. Quand vous demandez une dépendance, Angular remonte cet arbre jusqu'à trouver un provider. Cette hiérarchie est la clé pour comprendre où vos services sont instanciés et combien d'instances existent.
// Injector hierarchy visualization
//
// Null Injector (throws NullInjectorError)
// ↑
// Platform Injector (platformBrowserDynamic)
// ↑
// Root Injector (providedIn: 'root' services live here)
// ↑
// Route Injector (lazy-loaded modules create their own)
// ↑
// Element Injector (component providers: [...])
// ↑
// Child Element Injector (child components)
// Example: Understanding where instances are created
@Injectable({ providedIn: 'root' })
export class AuthService {
// Single instance in Root Injector
// Shared across entire application
}
@Component({
providers: [FormStateService] // Element Injector
})
export class UserFormComponent {
// Each UserFormComponent gets its own FormStateService
private formState = inject(FormStateService);
}La recherche suit cet ordre : Element Injector → Parent Element Injectors → Environment Injector (Module/Route) → Root → Platform → Null Injector. Si le Null Injector est atteint, Angular lève une erreur (sauf si la dépendance est optionnelle).
providedIn : Au-delà de 'root'
providedIn: 'root'
// 'root' - Application-wide singleton (most common)
@Injectable({ providedIn: 'root' })
export class AuthService {}
// 'platform' - Shared across multiple Angular apps on same page
@Injectable({ providedIn: 'platform' })
export class SharedAnalyticsService {}
// 'any' - One instance per lazy-loaded module boundary
@Injectable({ providedIn: 'any' })
export class ModuleScopedCache {}
// Factory with dependencies - tree-shakeable defaults
export const API_CONFIG = new InjectionToken<ApiConfig>('API_CONFIG', {
providedIn: 'root',
factory: () => ({
baseUrl: inject(ENVIRONMENT).apiUrl,
timeout: 30000,
retries: 3
})
});
// Route-level providers (best for lazy-loaded features)
export const FEATURE_ROUTES: Routes = [{
path: 'admin',
loadComponent: () => import('./admin.component'),
providers: [
AdminService, // Only loaded when route is accessed
{ provide: FEATURE_FLAG, useValue: 'admin-v2' }
]
}];Pourquoi ça compte : Avec providedIn: 'root'
Modificateurs de Résolution
Angular fournit plusieurs options pour contrôler comment et où les dépendances sont résolues. Comprendre ces modificateurs est essentiel pour des architectures complexes.
Dépendances Optionnelles
Par défaut, une dépendance manquante provoque une erreur. L'option optional: truenull
@Component({...})
export class FeatureComponent {
// Returns null if not provided, instead of throwing
private analytics = inject(AnalyticsService, { optional: true });
trackEvent(name: string) {
this.analytics?.track(name);
}
}
// Common pattern: feature flags with defaults
@Component({...})
export class DashboardComponent {
private featureFlags = inject(FEATURE_FLAGS, { optional: true }) ?? {};
get showNewChart(): boolean {
return this.featureFlags['dashboard-v2'] ?? false;
}
}self et skipSelf : Contrôler la Portée de Recherche
Ces modificateurs limitent où Angular cherche le provider :
// self: true - Only look in current element's injector
@Directive({ selector: '[validateOnBlur]' })
export class ValidateOnBlurDirective {
// Must be provided on this exact element, won't search parents
private ngControl = inject(NgControl, { self: true });
}
// skipSelf: true - Skip current injector, start from parent
@Component({
selector: 'nested-menu',
providers: [MenuService] // New instance for this subtree
})
export class NestedMenuComponent {
// Gets parent's MenuService, not our own
private parentMenu = inject(MenuService, { skipSelf: true, optional: true });
// Gets our own MenuService
private selfMenu = inject(MenuService);
constructor() {
this.parentMenu?.registerChild(this.selfMenu);
}
}Cas d'usage typique : Un composant formulaire qui veut s'enregistrer auprès d'un formulaire parent tout en ayant sa propre instance de FormGroupDirective
host : S'Arrêter aux Limites du Composant Hôte
host: true
@Directive({ selector: '[tooltip]' })
export class TooltipDirective {
// Only finds TooltipConfig if provided by host component
private config = inject(TooltipConfig, { host: true, optional: true });
}
@Component({
selector: 'data-table',
providers: [{ provide: TABLE_CONFIG, useValue: { striped: true } }],
template: `<ng-content></ng-content>`
})
export class DataTableComponent {}
@Component({ selector: 'table-row', template: `...` })
export class TableRowComponent {
// Gets config from DataTable host
private config = inject(TABLE_CONFIG, { host: true });
}InjectionToken : Injecter des Valeurs Non-Classes
Les classes sont leur propre token d'injection. Pour les valeurs primitives ou objets de configuration, utilisez InjectionToken
export const API_URL = new InjectionToken<string>('API_URL');
export interface AppConfig {
apiUrl: string;
features: Record<string, boolean>;
environment: 'dev' | 'staging' | 'prod';
}
export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');
// Token with tree-shakeable factory default
export const LOGGER = new InjectionToken<Logger>('LOGGER', {
providedIn: 'root',
factory: () => {
const config = inject(APP_CONFIG, { optional: true });
return config?.environment === 'prod'
? new ProductionLogger()
: new ConsoleLogger();
}
});
// Providing tokens
export const appConfig: ApplicationConfig = {
providers: [
{ provide: APP_CONFIG, useValue: environment },
{ provide: API_URL, useFactory: () => inject(APP_CONFIG).apiUrl }
]
};Multi-Providers : Collecter Plusieurs Implémentations
Le flag multi: true
export const CUSTOM_VALIDATORS = new InjectionToken<Validator[]>('CUSTOM_VALIDATORS');
export const appConfig: ApplicationConfig = {
providers: [
{ provide: CUSTOM_VALIDATORS, useClass: RequiredFieldsValidator, multi: true },
{ provide: CUSTOM_VALIDATORS, useClass: EmailFormatValidator, multi: true },
]
};
@Injectable({ providedIn: 'root' })
export class ValidationService {
private validators = inject(CUSTOM_VALIDATORS); // Validator[]
validate(form: FormGroup): ValidationError[] {
return this.validators.flatMap(v => v.validate(form));
}
}Stratégies de Providers de Composants
Déclarer des providers au niveau du composant crée une nouvelle instance par composant :
@Component({
selector: 'product-editor',
providers: [ProductEditorState], // New instance per component
template: `
<h2>{{ state.product()?.name }}</h2>
<product-form />
<product-preview />
`
})
export class ProductEditorComponent {
state = inject(ProductEditorState);
}
// Child components share parent's instance
@Component({ selector: 'product-form', template: `<form>...</form>` })
export class ProductFormComponent {
private state = inject(ProductEditorState); // Same instance as parent
}
// viewProviders: Only visible to view, not content children
@Component({
selector: 'modal',
viewProviders: [ModalContext], // Not visible to <ng-content>
template: `<div class="modal"><ng-content /></div>`
})
export class ModalComponent {}inject() vs Injection par Constructeur
La fonction inject()
// Constructor injection - verbose
@Component({...})
export class OldStyleComponent {
constructor(
private userService: UserService,
@Optional() private analytics: AnalyticsService | null,
@Inject(APP_CONFIG) private config: AppConfig
) {}
}
// inject() - cleaner, more flexible
@Component({...})
export class ModernComponent {
private userService = inject(UserService);
private analytics = inject(AnalyticsService, { optional: true });
private config = inject(APP_CONFIG);
private destroyRef = inject(DestroyRef);
constructor() {
this.destroyRef.onDestroy(() => this.cleanup());
}
}
// Extracting reusable injection logic
function injectRouteParam(name: string): Signal<string | null> {
const route = inject(ActivatedRoute);
return toSignal(route.paramMap.pipe(map(p => p.get(name))));
}
@Component({...})
export class ProductComponent {
private productId = injectRouteParam('id');
}Patterns de Test
L'injection de dépendances facilite grandement les tests :
describe('UserComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{
provide: UserService,
useValue: { getUser: vi.fn().mockReturnValue(of({ name: 'Test' })) }
}
]
});
});
it('should display user name', () => {
const fixture = TestBed.createComponent(UserComponent);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('Test');
});
});
describe('AuthService Integration', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideHttpClient(), provideHttpClientTesting()]
});
});
it('should authenticate user', () => {
const service = TestBed.inject(AuthService);
const httpTesting = TestBed.inject(HttpTestingController);
service.login('user', 'pass').subscribe(r => expect(r.token).toBeDefined());
httpTesting.expectOne('/api/auth/login').flush({ token: 'abc123' });
});
});Bonnes Pratiques
- Utilisez
plutôt que l'injection par constructeurinject() - Préférez
pour les services stateless partagésprovidedIn: 'root' - Utilisez des providers de composant pour l'état spécifique à une instance
- Les
avec factory permettent un tree-shaking optimalInjectionToken - Évitez les dépendances circulaires —
est un palliatifforwardRef()