Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
Angular

Principes de l’injection de dépendances

Un composant qui crée ses dépendances avec new obtient sa propre copie et reste lié à une implémentation précise. Avec l’injection de dépendances, il les demande avec inject(), et Angular les fournit....

0 chapitres 0 QCM Accès gratuit
Dans cet article

À propos de ce cours

Un composant qui crée ses dépendances avec new obtient sa propre copie et reste lié à une implémentation précise. Avec l’injection de dépendances, il les demande avec inject(), et Angular les fournit. @Injectable({ providedIn: 'root' }) crée un service unique partagé par toute l’application, alors que providers donne une instance par composant, que ses enfants partagent. Les recettes useClass, useExisting, useValue, useFactory et les InjectionToken permettent de remplacer une implémentation sans toucher aux composants. inject() ne s’appelle que pendant la création de l’objet. Le cours se termine par 5 activités corrigées et les erreurs fréquentes.

🎬 La correction du TP en vidéo : la TodoList construite partie par partie (17 min 11)

La vidéo ne s'affiche pas ? La regarder sur YouTube.

  1. Objectifs

    • Comprendre ce qu'est une dépendance, et pourquoi un composant ne devrait pas créer lui-même les siennes avec new
    • Créer un service avec @Injectable({ providedIn: 'root' }) et le demander avec inject()
    • Identifier les trois acteurs de l'injection : le jeton, le fournisseur et l'injecteur
    • Savoir où et quand appeler inject() (le contexte d'injection)
    • Choisir la portée d'un service : toute l'application ou un seul composant (la hiérarchie des injecteurs)
    • Remplacer une implémentation (useClass, useValue, useExisting), fournir une valeur avec un InjectionToken, rendre une dépendance facultative
    • Lire l'ancienne écriture : l'injection par le constructeur
  1. Présentation

    • Le chapitre précédent a montré comment des composants voisins communiquent par leurs entrées et leurs sorties. Mais certaines données et certains traitements n'appartiennent à aucun composant en particulier : un panier, un journal, une configuration, des notifications, un accès à une API.
    • On les place dans des services, et Angular les distribue aux composants qui en ont besoin : c'est l'injection de dépendances (dependency injection, DI). Le composant ne fabrique plus ses outils, il les demande.
    • Ce mécanisme est au cœur d'Angular : le routeur, le client HTTP et les formulaires fonctionnent tous ainsi. Ce chapitre en présente les principes ; les chapitres suivants les appliqueront.

Créer ses dépendances avec new ou les demander avec inject()

  • Pour suivre ce chapitre, continuez dans le projet premiers-pas-angular et créez un dossier src/app/injection/. Tous les exemples ont été testés avec Angular 22 (composants OnPush par défaut, sans Zone.js).
  1. Le problème : créer soi-même ses dépendances

    • Une dépendance est un objet dont une classe a besoin pour travailler. Ici, deux pages ont besoin d'un compteur de visites. La solution la plus directe est de le créer avec new:
import { Component, signal } from '@angular/core';

// Une classe ordinaire : chaque « new » crée un compteur indépendant
export class CompteurVisites {
  readonly total = signal(0);
  ajouter(): void {
    this.total.update((n) => n + 1);
  }
}

@Component({
  selector: 'app-page-accueil',
  template: `<button (click)="compteur.ajouter()">Visiter l'accueil</button> total vu par l'accueil : {{ compteur.total() }}`,
})
export class PageAccueil {
  protected readonly compteur = new CompteurVisites(); // le composant crée SA dépendance
}

@Component({
  selector: 'app-page-contact',
  template: `<button (click)="compteur.ajouter()">Visiter le contact</button> total vu par le contact : {{ compteur.total() }}`,
})
export class PageContact {
  protected readonly compteur = new CompteurVisites(); // … et celui-ci en crée une autre
}

@Component({
  selector: 'app-probleme-new',
  imports: [PageAccueil, PageContact],
  template: `<p><app-page-accueil /></p><p><app-page-contact /></p>`,
})
export class ProblemeNew {}
(2 clics sur « accueil », 1 clic sur « contact »)
Visiter l'accueil   total vu par l'accueil : 2
Visiter le contact  total vu par le contact : 1      ← chacun a SON compteur
  • Chaque new crée une instance indépendante : les deux pages ne voient pas le même total. Pour partager, il faudrait faire passer le compteur par un parent commun, avec des entrées et des sorties, à travers tous les niveaux.
  • Le composant est couplé à CompteurVisites: pour utiliser une autre implémentation (un compteur enregistré sur un serveur, un faux compteur pour les tests), il faut modifier son code.
  • Si le compteur avait lui-même besoin d'autres objets, le composant devrait aussi savoir les créer : new CompteurVisites(new Stockage(new Config()))…
  1. Le principe : demander au lieu de créer

    • L'injection de dépendances renverse la responsabilité (on parle d'inversion de contrôle) : le composant déclare ce dont il a besoin, et c'est Angular qui décide comment le créer et quelle instance lui donner.
    • La classe devient un service grâce au décorateur @Injectable. L'option providedIn: 'root' la rend disponible dans toute l'application, avec une seule instance, créée à la première demande.
    • Les composants la demandent avec la fonction inject(), importée de @angular/core.
import { Component, Injectable, computed, inject, signal } from '@angular/core';

// Un service : une classe marquée @Injectable, fournie à toute l'application
@Injectable({ providedIn: 'root' })
export class VisitesService {
  private readonly pages = signal<string[]>([]);
  readonly total = computed(() => this.pages().length);
  readonly derniere = computed(() => this.pages().at(-1) ?? 'aucune');

  ajouter(page: string): void {
    this.pages.update((p) => [...p, page]);
  }
}

@Component({
  selector: 'app-page-accueil-di',
  template: `<button (click)="visites.ajouter('accueil')">Visiter l'accueil</button> total : {{ visites.total() }}`,
})
export class PageAccueilDi {
  protected readonly visites = inject(VisitesService); // le composant DEMANDE sa dépendance
}

@Component({
  selector: 'app-page-contact-di',
  template: `<button (click)="visites.ajouter('contact')">Visiter le contact</button> total : {{ visites.total() }}`,
})
export class PageContactDi {
  protected readonly visites = inject(VisitesService);
}

@Component({
  selector: 'app-premier-service',
  imports: [PageAccueilDi, PageContactDi],
  template: `
    <p><app-page-accueil-di /></p>
    <p><app-page-contact-di /></p>
    <p>Dernière page visitée : {{ visites.derniere() }} · même instance partout : {{ memeInstance }}</p>
  `,
})
export class PremierService {
  protected readonly visites = inject(VisitesService);
  protected readonly memeInstance = this.visites === inject(VisitesService);
}
Visiter l'accueil   total : 0
Visiter le contact  total : 0
Dernière page visitée : aucune · même instance partout : true
(2 clics sur « accueil », 1 clic sur « contact »)
Visiter l'accueil   total : 3
Visiter le contact  total : 3
Dernière page visitée : contact · même instance partout : true
  • Les deux pages et leur parent reçoivent la même instance: chaque clic met à jour les trois affichages, sans entrée ni sortie.
  • L'état du service est un signal privé (pages) : les composants le lisent par des computed publics et le modifient par une méthode (ajouter). Le service garde ainsi le contrôle de ses données.
  • Le service n'est créé que s'il est demandé au moins une fois : un service jamais injecté est retiré du code final (tree-shaking).

Le jeton, le fournisseur et l'injecteur

  1. Un service qui dépend d'un autre service

    • Un service peut, lui aussi, demander ses dépendances avec inject(). Angular crée alors toute la chaîne dans le bon ordre : ici, Salutation a besoin de Horloge.
    • On rencontre aussi l'ancienne écriture, où la dépendance est un paramètre du constructeur. Les deux sont équivalentes ; inject() est l'écriture recommandée aujourd'hui.
import { Component, Injectable, inject } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class Horloge {
  heure(): number {
    return new Date().getHours();
  }
}

// Un service peut lui-même demander d'autres services
@Injectable({ providedIn: 'root' })
export class Salutation {
  private readonly horloge = inject(Horloge);

  pour(nom: string): string {
    const h = this.horloge.heure();
    return `${h < 18 ? 'Bonjour' : 'Bonsoir'} ${nom} (il est ${h} h)`;
  }
}

@Component({
  selector: 'app-dependances-entre-services',
  template: `
    <p>Avec inject() : {{ salutation.pour('Amira') }}</p>
    <p>Avec le constructeur : {{ salut2.pour('Sami') }}</p>
  `,
})
export class DependancesEntreServices {
  protected readonly salutation = inject(Salutation);

  // Ancienne écriture, équivalente : la dépendance est un paramètre du constructeur
  constructor(protected readonly salut2: Salutation) {}
}
Avec inject() : Bonjour Amira (il est 7 h)
Avec le constructeur : Bonjour Sami (il est 7 h)
  • inject() ne fonctionne que pendant la création de l'objet : dans l'initialisation d'une propriété, dans le constructeur ou dans une fonction de fabrique. Appelé dans une méthode (par exemple au clic), il échoue : NG0203: The `VisitesService` token injection failed. `inject()` function must be called from an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`.
  • Deux services qui se demandent mutuellement forment une boucle qu'Angular refuse : NG0200: Circular dependency detected for `Poule`. Path: Poule -> Oeuf -> Poule. Il faut alors revoir le découpage (souvent, un troisième service commun).
  • Pour migrer un projet existant vers inject(): ng generate @angular/core:inject.

Où peut-on appeler inject() ?

  1. La hiérarchie des injecteurs

    • providedIn: 'root' place le service dans l'injecteur racine: une instance pour toute l'application. Mais chaque composant peut aussi avoir son propre injecteur, en déclarant le service dans son tableau providers.
    • Quand un composant demande une dépendance, Angular la cherche d'abord dans son propre injecteur, puis dans celui de son parent, et ainsi de suite jusqu'à la racine. Le premier fournisseur trouvé l'emporte.
import { Component, Injectable, inject, input, signal } from '@angular/core';

// Pas de providedIn : ce service doit être fourni explicitement
@Injectable()
export class CompteurClics {
  readonly valeur = signal(0);
  private static suivant = 1;
  readonly id = CompteurClics.suivant++; // numéro de l'instance, pour la voir
}

// L'enfant demande un CompteurClics : il reçoit celui de la carte qui l'entoure
@Component({
  selector: 'app-bouton-plus',
  template: `<button (click)="compteur.valeur.set(compteur.valeur() + 1)">+1</button>`,
})
export class BoutonPlus {
  protected readonly compteur = inject(CompteurClics);
}

@Component({
  selector: 'app-carte-compteur',
  imports: [BoutonPlus],
  providers: [CompteurClics], // une instance par carte
  template: `
    <div class="carte"><strong>{{ titre() }}</strong> : {{ compteur.valeur() }} clic(s)
      <app-bouton-plus /> <app-bouton-plus /> <small>instance n° {{ compteur.id }}</small></div>
  `,
  styles: `.carte { border: 1px solid #cbd5e1; border-radius: 8px; padding: 6px 10px; margin: 6px 0; max-width: 420px; }`,
})
export class CarteCompteur {
  readonly titre = input('Carte');
  protected readonly compteur = inject(CompteurClics);
}

@Component({
  selector: 'app-hierarchie',
  imports: [CarteCompteur],
  template: `<app-carte-compteur titre="Carte A" /><app-carte-compteur titre="Carte B" />`,
})
export class Hierarchie {}
Carte A : 0 clic(s)  +1 +1  instance n° 1
Carte B : 0 clic(s)  +1 +1  instance n° 2
(les deux boutons de A, puis un bouton de B)
Carte A : 2 clic(s)  +1 +1  instance n° 1
Carte B : 1 clic(s)  +1 +1  instance n° 2

La hiérarchie des injecteurs : une instance par carte, partagée par ses enfants

  • @Injectable() sans providedIn: le service n'est disponible que là où on le fournit. Chaque carte crée sa propre instance (n° 1 et n° 2).
  • BoutonPlus ne déclare rien : il reçoit l'instance de la carte qui l'entoure. Les deux boutons d'une même carte partagent donc le même compteur.
  • Si aucun injecteur ne fournit le service, l'erreur est immédiate : NG0201: No provider found for `CompteurClics`.
  • Règle pratique : providedIn: 'root' pour un état partagé par toute l'application (panier, utilisateur connecté), providers d'un composant pour un état propre à chaque exemplaire (un formulaire, un questionnaire).
  1. Remplacer une implémentation

    • Le composant demande un jeton, pas une implémentation précise. Le jeton peut être une classe abstraite: inject(Journal) signifie « donne-moi un journal, peu importe lequel ». C'est le fournisseur qui choisit, avec une recette:
      • useClass: créer une instance de telle classe ;
      • useExisting: réutiliser une instance déjà fournie sous un autre jeton ;
      • useValue: fournir une valeur toute faite ;
      • useFactory: calculer la valeur avec une fonction.
    • Pour une simple valeur (un taux, une adresse d'API, une configuration), qui n'est pas une classe, on crée un jeton avec new InjectionToken<T>(). L'option factory lui donne une valeur par défaut.
import { Component, Injectable, InjectionToken, inject, signal } from '@angular/core';

// 1. Une classe abstraite sert de jeton : on demande « un Journal », sans savoir lequel
export abstract class Journal {
  abstract ecrire(message: string): void;
}

@Injectable()
export class JournalConsole extends Journal {
  ecrire(message: string): void {
    console.log('[journal]', message);
  }
}

@Injectable()
export class JournalMemoire extends Journal {
  readonly lignes = signal<string[]>([]);
  ecrire(message: string): void {
    this.lignes.update((l) => [...l, message]);
  }
}

// 2. Un jeton pour une simple valeur, avec une valeur par défaut (factory)
export const TVA = new InjectionToken<number>('TVA', { providedIn: 'root', factory: () => 19 }); // en %

@Component({
  selector: 'app-caisse',
  template: `
    <button (click)="vendre()">Vendre un article à 100 DT</button>
    TTC : {{ ttc }} DT (TVA {{ tva }} %)
  `,
})
export class Caisse {
  private readonly journal = inject(Journal);
  protected readonly tva = inject(TVA);
  protected readonly ttc = 100 + this.tva; // prix HT : 100 DT
  vendre(): void {
    this.journal.ecrire(`vente : ${this.ttc} DT`);
  }
}

@Component({
  selector: 'app-remplacer',
  imports: [Caisse],
  // Pour ce composant et ses enfants : un journal en mémoire et une TVA à 7 %
  providers: [JournalMemoire, { provide: Journal, useExisting: JournalMemoire }, { provide: TVA, useValue: 7 }],
  template: `<app-caisse /><ol>@for (l of memoire.lignes(); track $index) { <li>{{ l }}</li> }</ol>`,
})
export class Remplacer {
  protected readonly memoire = inject(JournalMemoire);
}

@Component({
  selector: 'app-remplacer-console',
  imports: [Caisse],
  providers: [{ provide: Journal, useClass: JournalConsole }],
  template: `<app-caisse /> <small>(voir la console)</small>`,
})
export class RemplacerConsole {}
Vendre un article à 100 DT   TTC : 107 DT (TVA 7 %)
(2 ventes)   1. vente : 107 DT   2. vente : 107 DT          ← journal en mémoire, affiché
Vendre un article à 100 DT   TTC : 119 DT (TVA 19 %)  (voir la console)
(1 vente)    console : [journal] vente : 119 DT             ← même Caisse, autre journal, TVA par défaut
  • Caisse est exactement le même composant dans les deux cas : seuls les providers de son parent changent. C'est ce qui rend le code testable: dans un test, on fournit un faux service à la place du vrai.
  • Le premier parent fournit JournalMemoire, puis indique que le jeton Journal désigne cette même instance (useExisting) : ainsi la caisse écrit dans le journal que le parent affiche.
  • Le second parent n'a pas de fournisseur pour TVA: la valeur par défaut du jeton (19) est utilisée.
  • Une classe abstraite ou un jeton sans fournisseur provoque NG0201: No provider found for `Journal`. ou NG0201: No provider found for `InjectionToken URL_API`.

Les différentes façons de fournir une dépendance

  1. Une dépendance facultative

    • Par défaut, une dépendance introuvable est une erreur. Avec l'option { optional: true }, inject() renvoie null à la place, et le type devient T | null.
import { Component, Injectable, inject, signal } from '@angular/core';

@Injectable()
export class Statistiques {
  readonly clics = signal(0);
}

@Component({
  selector: 'app-bouton-suivi',
  template: `<button (click)="cliquer()">Cliquer</button> {{ stats ? stats.clics() + ' clic(s) suivis' : 'statistiques désactivées' }}`,
})
export class BoutonSuivi {
  // optional : null si aucun fournisseur, au lieu d'une erreur
  protected readonly stats = inject(Statistiques, { optional: true });
  cliquer(): void {
    this.stats?.clics.update((n) => n + 1);
  }
}

@Component({
  selector: 'app-zone-suivie',
  imports: [BoutonSuivi],
  providers: [Statistiques],
  template: `<app-bouton-suivi />`,
})
export class ZoneSuivie {}

@Component({
  selector: 'app-optionnel',
  imports: [BoutonSuivi, ZoneSuivie],
  template: `<p>Dans une zone suivie : <app-zone-suivie /></p><p>Ailleurs : <app-bouton-suivi /></p>`,
})
export class Optionnel {}
Dans une zone suivie : Cliquer  0 clic(s) suivis    → (2 clics) 2 clic(s) suivis
Ailleurs :              Cliquer  statistiques désactivées
  • Le même bouton fonctionne avec ou sans statistiques : il teste simplement stats (?. dans la méthode, condition dans le template).
  • D'autres options existent : self (chercher uniquement dans l'injecteur du composant), skipSelf (commencer par le parent) et host.

Correspondance entre l'injection par le constructeur et inject()

  1. Aide-mémoire

Aide-mémoire de l'injection de dépendances dans Angular

  1. Activité 1 : un service de notifications

    • Créer NotificationService (providedIn: 'root') :
      • un signal privé de notifications { id, texte, type }, où le type vaut 'succes', 'erreur' ou 'info', exposé en lecture seule avec asReadonly();
      • afficher(texte, type, duree = 3000) ajoute une notification et la retire après la durée donnée (0 : elle reste) ; fermer(id) la retire.
    • Trois composants indépendants l'injectent : ZoneNotifications affiche la liste (couleur selon le type, bouton ×, « Aucune notification. ») ; FormulaireContact vérifie qu'un e-mail contient « @ » et affiche « Adresse e-mail invalide. » ou « Message envoyé à … » ; BoutonCopier affiche « Lien copié. » sans durée.

Résultat attendu de l'activité 1 : trois notifications

  • Vérifier : après 3 secondes, les deux premières notifications disparaissent ; « Lien copié. » reste jusqu'au clic sur ×.
  • Pourquoi exposer liste avec asReadonly()? Que se passerait-il si un composant appelait liste.set([])?
  1. Activité 2 : un panier partagé

    • Créer PanierService (providedIn: 'root') avec un signal privé de lignes { produit, quantite }, deux computed (nombre d'articles et total) et trois méthodes : ajouter(produit) (augmente la quantité si le produit est déjà là), retirer(id) (diminue, et supprime la ligne à 0) et vider().
    • Trois composants l'injectent, sans aucune entrée ni sortie: IconePanier (badge avec le nombre d'articles), Catalogue (trois produits et un bouton « Ajouter ») et ResumePanier (lignes, bouton −, total, « Le panier est vide. » et un bouton « Vider »).

Résultat attendu de l'activité 2 : le panier partagé

  • Vérifier : Clavier, Clavier, Écran → badge 3, total 480 DT ; − sur Clavier puis Souris → badge 3, total 455 DT.
  • Remplacer inject(PanierService) par new PanierService() dans Catalogue: que se passe-t-il, et pourquoi ?
  • Cette activité est corrigée pas à pas dans la vidéo de la leçon.
  1. Activité 3 : un journal remplaçable

    • Créer une classe abstraite Journal (méthode ecrire(message)) et deux implémentations : JournalConsole (console.log, fournie à la racine) et JournalMemoire (un signal des 5 dernières lignes, précédées de l'heure).
    • Le composant Calculatrice (deux champs et un bouton =) injecte Journal et y écrit chaque calcul (« 6 × 7 = 42 »), sans savoir quel journal il utilise.
    • Deux postes l'affichent : PosteMemoire fournit JournalMemoire et { provide: Journal, useExisting: JournalMemoire }, puis affiche les lignes ; PosteConsole fournit { provide: Journal, useClass: JournalConsole }.

Résultat attendu de l'activité 3 : deux postes, deux journaux

  • Ouvrir la console (F12) : seuls les calculs du poste 2 y apparaissent.
  • Que se passe-t-il si PosteMemoire utilise useClass: JournalMemoire au lieu de useExisting? (indice : combien d'instances ?)
  1. Activité 4 : une configuration injectée

    • Créer l'interface ConfigBoutique (devise, tva, livraisonOfferteDes) et le jeton CONFIG_BOUTIQUE, avec une valeur par défaut (factory) : TND, 19 %, livraison offerte dès 200.
    • Le composant Prix (entrées nom et ht) injecte la configuration et affiche le prix TTC avec le pipe currency, le taux, et « livraison offerte » ou « livraison 7 ».
    • Vitrine affiche deux prix (Casque 100, Écran 250). VitrineExport affiche la même vitrine, mais fournit une autre configuration avec useValue: EUR, 0 %, livraison offerte dès 100.

Résultat attendu de l'activité 4 : deux configurations

  • Vérifier : Casque 119,00 TND (livraison 7), Écran 297,50 TND (offerte) ; à l'export, 100,00 € et 250,00 €, livraison offerte.
  • Pourquoi un InjectionToken et pas une classe ? Pourquoi ne pas écrire simplement une constante exportée ?
  1. Activité 5 : des questionnaires indépendants

    • Créer ReponsesService avec @Injectable() sans providedIn: un signal privé Record<string, boolean>, deux computed (nombre de réponses et nombre de oui), et les méthodes repondre(question, valeur) et valeur(question).
    • Questionnaire (entrées titre et questions) fournit le service dans ses providers et affiche « 2 / 2 réponse(s) · 1 « oui » ». Chaque QuestionOuiNon injecte le service de son questionnaire et met en évidence le bouton choisi.
    • La page affiche deux questionnaires : « Avant le cours » (2 questions) et « Après le cours » (3 questions). Leurs réponses ne doivent jamais se mélanger.

Résultat attendu de l'activité 5 : deux questionnaires indépendants

  • Déplacer le service en providedIn: 'root' et retirer les providers: que deviennent les compteurs ? Et si deux questions portent le même texte ?
  • Pourquoi QuestionOuiNon n'a-t-il besoin ni d'entrée pour le service, ni de sortie pour la réponse ?
  • Les corrigés des cinq activités, testés avec Angular 22, sont fournis dans l'archive de la leçon (dossier corriges/).
  1. Erreurs fréquentes

Message ou symptôme Cause Solution
NG0201: No provider found for `CompteurClics`. Service sans providedIn et sans providers sur le chemin providedIn: 'root' ou providers: [CompteurClics] sur un parent
NG0201: No provider found for `Journal`. Classe abstraite demandée sans recette { provide: Journal, useClass: … }
NG0201: No provider found for `InjectionToken URL_API`. Jeton sans factory ni fournisseur Ajouter factory ou { provide: URL_API, useValue: … }
NG0201: No provider found for `Ordinaire`. Classe sans @Injectable ni fournisseur Ajouter @Injectable({ providedIn: 'root' })
NG0203: … `inject()` function must be called from an injection context … inject() dans une méthode ou un rappel Injecter dans un champ, l'utiliser dans la méthode
NG0200: Circular dependency detected for `Poule`. Deux services qui se demandent mutuellement Revoir le découpage (service commun)
Deux composants n'affichent pas le même état new MonService() au lieu de inject() Toujours inject(MonService)
L'état n'est pas partagé entre deux parties de la page Service aussi déclaré dans les providers d'un composant Retirer ce providers si l'état doit être global
Les réponses de deux formulaires se mélangent Service providedIn: 'root' au lieu d'un service par composant providers: [MonService] sur le composant
La vue ne se met pas à jour État du service dans une propriété ordinaire Utiliser des signals (signal, computed)
Un composant modifie l'état du service de l'extérieur Signal public modifiable Signal privé + asReadonly() + méthodes
  1. À retenir

    • Une dépendance n'est pas créée par celui qui l'utilise : il la demande avec inject(), et Angular la fournit.
    • @Injectable({ providedIn: 'root' }) crée un service unique, partagé par toute l'application, créé à la première demande.
    • Trois acteurs : le jeton (ce que l'on demande), le fournisseur (la recette) et l'injecteur (qui fabrique et garde l'instance).
    • inject() s'appelle pendant la création : champ, constructeur, fabrique ; jamais dans une méthode.
    • Les injecteurs forment une hiérarchie : providers dans un composant donne une instance par composant, partagée par ses enfants.
    • useClass, useExisting, useValue, useFactory et InjectionToken permettent de remplacer une implémentation sans toucher aux composants.
    • Un bon service garde son état privé (signals), l'expose en lecture seule et le modifie par des méthodes.
Un composant qui demande ses outils au lieu de les fabriquer est plus simple, plus facile à tester et plus facile à faire évoluer : c'est tout l'intérêt de l'injection de dépendances.