AngularTypeScriptDependency InjectionArchitectureFrontend

Comprenez providers, inject(), InjectionToken et injecteurs hiérarchiques dans Angular, puis choisissez le bon scope sans créer de singletons accidentels.

Yva Hajatiana
14 août 2026
8 min de lecture
Partager :X / TwitterLinkedIn
Graphe abstrait représentant des dépendances et des flux Angular

Injection de dépendances Angular : guide pratique

L'injection de dépendances d'Angular ne sert pas seulement à rendre un service accessible dans un composant. Elle définit où une instance est créée, combien de temps elle vit, quels descendants peuvent la voir et comment une implémentation peut être remplacée dans un test ou une partie de l'application.

Les bugs arrivent rarement sur la syntaxe de base. Ils apparaissent quand un service supposé unique est recréé dans un composant, quand un état local devient global ou quand un InjectionToken est utilisé sans type ni fabrique sûre.

Ce guide utilise des composants standalone et la fonction inject(), tout en montrant l'équivalent par constructeur lorsque cela clarifie le fonctionnement.

Le principe : recevoir au lieu de construire

Sans injection, une classe choisit et construit elle-même sa dépendance :

export class OrdersFacade {
  private api = new OrdersApiClient('/api');
}

OrdersFacade dépend maintenant d'une URL, d'une implémentation HTTP et de sa construction. Le test doit subir ces choix.

Avec l'injection, la classe demande un objet déjà configuré :

import { Injectable, inject } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class OrdersFacade {
  private readonly api = inject(OrdersApiClient);
}

Angular associe le token OrdersApiClient à un provider, trouve l'injecteur compétent et retourne l'instance correspondante. La classe consommatrice ne décide plus de la construction.

Fournir puis injecter

Le système possède deux opérations distinctes :

  1. fournir une valeur pour un token ;
  2. injecter la valeur associée à ce token.

Le cas le plus courant est un service disponible à la racine :

import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class AuditLogger {
  record(event: string): void {
    // Envoyer l'événement vers la vraie infrastructure.
  }
}

providedIn: 'root' permet généralement de partager une seule instance dans l'application et autorise le tree-shaking si le service n'est jamais utilisé.

Dans un composant :

import { ChangeDetectionStrategy, Component, inject } from '@angular/core';

@Component({
  selector: 'app-checkout',
  standalone: true,
  template: `<button (click)="confirm()">Confirmer</button>`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CheckoutComponent {
  private readonly audit = inject(AuditLogger);

  confirm(): void {
    this.audit.record('checkout_confirmed');
  }
}

L'injection par constructeur reste valide :

constructor(private readonly audit: AuditLogger) {}

inject() est particulièrement pratique dans les initialiseurs de champs, les gardes fonctionnels et les fabriques. Il ne peut toutefois être appelé que dans un contexte d'injection : pendant la construction gérée par Angular ou dans une fonction explicitement exécutée dans ce contexte. L'appeler plus tard dans un gestionnaire de clic produit une erreur.

Choisir le bon scope

Le niveau où vous fournissez le service détermine sa visibilité et son cycle de vie.

Emplacement du providerInstance obtenueUsage typique
providedIn: 'root'partagée à l'échelle de l'applicationclient API, configuration, authentification
ApplicationConfig.providersapplicationproviders fonctionnels et configuration globale
route providersisolée dans la branche de routefonctionnalité lazy, état de feature
composant providersune instance par composantétat local complexe, façade d'écran
composant viewProviderslimitée à la vue du composantdépendance interne non visible au contenu projeté

Une instance par composant

Supposons qu'un éditeur de commande conserve un brouillon :

@Injectable()
export class OrderDraftStore {
  // État du brouillon courant.
}

@Component({
  selector: 'app-order-editor',
  standalone: true,
  providers: [OrderDraftStore],
  templateUrl: './order-editor.html',
})
export class OrderEditorComponent {
  readonly draft = inject(OrderDraftStore);
}

Chaque instance de OrderEditorComponent reçoit son propre store. Le fournir à la racine aurait mélangé les brouillons de plusieurs éditeurs ouverts en même temps.

À l'inverse, ajouter un service global dans providers d'un composant crée silencieusement une nouvelle instance. C'est une cause fréquente de sessions incohérentes et de caches dupliqués.

Comprendre la hiérarchie des injecteurs

Angular utilise notamment une hiérarchie d'EnvironmentInjector pour l'application et les routes, ainsi qu'une hiérarchie d'ElementInjector liée aux composants et directives.

Quand un composant demande un token, Angular cherche à partir de l'emplacement courant puis remonte vers ses parents. Il ne descend jamais dans les injecteurs des enfants.

Cette règle permet de spécialiser une dépendance pour une sous-arborescence :

@Component({
  selector: 'app-preview-area',
  standalone: true,
  providers: [
    { provide: DocumentStore, useClass: InMemoryDocumentStore },
  ],
  template: `<app-document-editor />`,
})
export class PreviewAreaComponent {}

Les descendants de PreviewAreaComponent reçoivent InMemoryDocumentStore. Le reste de l'application continue d'utiliser le provider défini plus haut.

Les options de résolution permettent d'exprimer une intention particulière :

const parentStore = inject(OrderDraftStore, { skipSelf: true });
const optionalTelemetry = inject(Telemetry, { optional: true });

Utilisez-les avec parcimonie. Si une classe nécessite plusieurs combinaisons de skipSelf, self ou host, la structure des providers mérite probablement d'être simplifiée.

Fournir une classe, une valeur, une fabrique ou un alias

Un provider n'est pas nécessairement une classe.

useClass : substituer une implémentation

providers: [
  { provide: PaymentGateway, useClass: SandboxPaymentGateway },
]

Le token demandé reste PaymentGateway, mais Angular construit SandboxPaymentGateway.

useValue : fournir une configuration immuable

Les interfaces TypeScript disparaissent à l'exécution et ne peuvent donc pas servir directement de token. Utilisez InjectionToken :

import { InjectionToken } from '@angular/core';

export interface ApiConfig {
  baseUrl: string;
  timeoutMs: number;
}

export const API_CONFIG = new InjectionToken<ApiConfig>('API_CONFIG');

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: API_CONFIG,
      useValue: Object.freeze({
        baseUrl: '/api',
        timeoutMs: 5_000,
      }),
    },
  ],
};

Puis :

const config = inject(API_CONFIG);

Évitez une chaîne brute comme token : deux bibliothèques peuvent choisir le même nom et le type est perdu.

useFactory : construire à partir d'autres dépendances

export const API_URL = new InjectionToken<string>('API_URL', {
  providedIn: 'root',
  factory: () => {
    const environment = inject(APP_ENVIRONMENT);
    return environment.production ? 'https://api.example.com' : '/api';
  },
});

La fabrique reste déterministe et testable. N'y placez pas un appel réseau ou une initialisation lourde cachée : l'injection ne doit pas surprendre le consommateur avec un effet de bord coûteux.

useExisting : créer un alias sans dupliquer l'instance

providers: [
  ConsoleAuditLogger,
  { provide: AuditLogger, useExisting: ConsoleAuditLogger },
]

Avec useExisting, les deux tokens pointent vers la même instance. useClass aurait créé une instance supplémentaire.

Les providers fonctionnels des bibliothèques Angular

Dans une application standalone moderne, les bibliothèques exposent souvent une fonction de configuration :

import { provideHttpClient, withInterceptors } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withInterceptors([authInterceptor])),
  ],
};

Cette forme regroupe les tokens nécessaires et permet une configuration explicite au bootstrap. Préférez l'API officielle de la bibliothèque à la reconstruction manuelle de ses providers internes.

Tester en remplaçant une dépendance

L'un des bénéfices concrets de la DI est de remplacer une frontière externe :

describe('CheckoutComponent', () => {
  const audit = jasmine.createSpyObj<AuditLogger>('AuditLogger', ['record']);

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [CheckoutComponent],
      providers: [
        { provide: AuditLogger, useValue: audit },
      ],
    }).compileComponents();
  });

  it('records the confirmation', () => {
    const fixture = TestBed.createComponent(CheckoutComponent);

    fixture.componentInstance.confirm();

    expect(audit.record).toHaveBeenCalledWith('checkout_confirmed');
  });
});

Remplacez les services qui représentent une frontière : réseau, stockage, horloge, navigateur ou télémétrie. Créer des mocks de toutes les petites classes métier peut au contraire coupler les tests à l'implémentation.

Les erreurs fréquentes

Fournir partout « au cas où »

Un service fourni à la racine n'a pas besoin d'être répété dans chaque composant. Chaque répétition peut créer une nouvelle instance et masquer le provider attendu.

Utiliser un service comme fourre-tout global

La DI facilite l'accès, mais ne justifie pas un service qui mélange authentification, appels HTTP, état d'écran et notifications. Gardez des responsabilités cohérentes.

Confondre singleton et immutabilité

Une instance partagée peut contenir un état modifiable. Documentez ce qui est global et préférez des APIs en lecture seule lorsque tous les consommateurs ne doivent pas modifier cet état.

Appeler inject() hors contexte

Ce code est incorrect :

save(): void {
  const api = inject(OrdersApiClient); // Trop tard : méthode ordinaire.
}

Injectez dans un champ ou le constructeur, puis utilisez la référence dans la méthode.

Masquer une dépendance optionnelle importante

optional: true retourne null si aucun provider n'existe. Utilisez-le uniquement lorsque l'absence a un comportement métier explicite ; sinon laissez Angular signaler l'erreur de configuration.

Une grille de décision

QuestionDécision probable
Tous les écrans partagent-ils cette capacité ?provider racine
Chaque occurrence du composant doit-elle isoler son état ?providers du composant
La dépendance appartient-elle à une feature lazy ?provider de route
Le token représente-t-il une configuration ou une interface ?InjectionToken<T>
Faut-il choisir une implémentation selon d'autres dépendances ?useFactory
Deux tokens doivent-ils partager exactement la même instance ?useExisting
Le composant doit-il cacher le provider au contenu projeté ?viewProviders

Le bon scope est le plus étroit qui corresponde réellement au cycle de vie attendu, sans recréer une dépendance supposée partagée.

Ressources officielles

La documentation Angular présente les fondamentaux de l'injection de dépendances, la définition des différents types de providers, le fonctionnement des injecteurs hiérarchiques et les contraintes de la fonction inject().

L'injection de dépendances devient utile quand elle rend explicites les frontières et les cycles de vie. Si vous savez répondre à « qui fournit cette valeur, où vit-elle et qui peut la remplacer ? », votre configuration Angular restera compréhensible lorsque l'application grandira.

Mots-clés :AngularTypeScriptDependency InjectionArchitectureFrontend
Y

Yva Hajatiana

Articles techniques sur l'ingénierie logicielle, .NET, le cloud et l'intelligence artificielle appliquée aux applications.