← Retour au blog
Angular Publié le 2025-12-24 · 15 min de lecture

Injection de Dépendances Angular : Guide Avancé

Maîtrisez le système DI d'Angular avec la hiérarchie d'injecteurs, les modificateurs de résolution et les patterns de production

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.

typescript
// 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' est le choix par défaut, mais d'autres options permettent un contrôle plus fin du tree-shaking et de la portée :

typescript
// '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', le service est inclus dans le bundle principal même si seule une route lazy-loaded l'utilise. Utiliser des providers au niveau des routes permet d'isoler le code dans les chunks appropriés.

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: true retourne null si le provider n'existe pas :

typescript
@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 :

typescript
// 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 limite la recherche à l'injecteur du composant hôte. Utile pour les directives qui communiquent avec leur composant conteneur :

typescript
@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 :

typescript
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 permet à plusieurs providers de contribuer au même token :

typescript
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 :

typescript
@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() (Angular 14+) offre des avantages significatifs :

typescript
// 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 :

typescript
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 inject() plutôt que l'injection par constructeur
  • Préférez providedIn: 'root' pour les services stateless partagés
  • Utilisez des providers de composant pour l'état spécifique à une instance
  • Les InjectionToken avec factory permettent un tree-shaking optimal
  • Évitez les dépendances circulaires — forwardRef() est un palliatif
angulardependency-injectionservicesarchitecture

Créé avec Angular & PrimeNG

© 2025 Blog Java Angular