Découvrez inject() d'Angular 19 : injection de dépendances sans constructeur, composables réutilisables, InjectionToken et options avancées avec exemples pratiques.
Pourquoi inject() ? Le problème du constructeur
L'injection de dépendances est au cœur d'Angular depuis sa première version. Pendant longtemps, la seule façon d'injecter un service dans un composant était le constructeur. Cette approche fonctionne, mais elle a ses limites quand le projet grossit.
Voici un composant classique avec plusieurs dépendances dans le constructeur :
// ❌ Ancienne approche — constructeur encombré
@Component({ selector: 'app-fiche-produit', /* ... */ })
export class FicheProduitComponent implements OnInit {
produit?: Produit;
avisUtilisateurs: Avis[] = [];
constructor(
private route: ActivatedRoute, // lire l'ID dans l'URL
private produitsService: ProduitsService,
private avisService: AvisService,
private panier: PanierService,
private analytics: AnalyticsService,
private toastService: ToastService,
private router: Router
) {}
ngOnInit() {
const id = this.route.snapshot.paramMap.get('id');
// ... initialisation
}
ajouterAuPanier() {
this.panier.ajouter(this.produit!);
this.analytics.track('add_to_cart', { id: this.produit!.id });
this.toastService.succes('Produit ajouté au panier');
}
}
Ce constructeur avec 7 paramètres est difficile à lire, et chaque nouveau service allonge encore la liste. Les tests unitaires deviennent pénibles car il faut mocker tous les paramètres même ceux non utilisés dans le test.
Avec inject(), le même composant s'écrit ainsi :
// ✅ Nouvelle approche Angular 19 — inject() en propriétés de classe
@Component({ selector: 'app-fiche-produit', standalone: true, /* ... */ })
export class FicheProduitComponent {
// Chaque dépendance est déclarée sur sa propre ligne, clairement nommée
private route = inject(ActivatedRoute);
private produitsService = inject(ProduitsService);
private avisService = inject(AvisService);
private panier = inject(PanierService);
private analytics = inject(AnalyticsService);
private toastService = inject(ToastService);
private router = inject(Router);
// Plus de constructeur — Angular injecte automatiquement au démarrage
}
inject() n'est pas "plus rapide" — c'est plus lisible, plus testable, et surtout il ouvre la porte aux composables (des fonctions réutilisables qui encapsulent plusieurs injections).
La syntaxe de base pas à pas
inject() s'importe depuis @angular/core et s'appelle avec le token à injecter comme argument :
// Importer inject depuis @angular/core
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
@Component({
selector: 'app-exemple',
standalone: true,
template: `<p>Exemple inject()</p>`
})
export class ExempleComponent {
// Injection de base — TypeScript infère automatiquement le type
private http = inject(HttpClient); // type : HttpClient
private router = inject(Router); // type : Router
naviguer() {
this.router.navigate(['/accueil']);
}
}
Contextes valides pour inject()
La règle la plus importante à retenir : inject() ne peut être appelé que dans un contexte d'injection. Voici les endroits valides et invalides :
| Contexte | Valide ? | Exemple |
|---|---|---|
| Propriétés de classe (field initializer) | ✓ Oui | private svc = inject(MonService) |
| Corps du constructeur | ✓ Oui | constructor() { this.svc = inject(MonService) } |
Factory de provider (useFactory) |
✓ Oui | useFactory: () => inject(ConfigService).init() |
Méthodes de classe (ngOnInit, handlers) |
✗ Non | Erreur NG0203 : pas de contexte |
Callbacks asynchrones (setTimeout, then) |
✗ Non | Erreur NG0203 : contexte perdu |
// ❌ Mauvais usage : inject() dans une méthode
@Component({ /* ... */ })
export class MauvaisExempleComponent {
ngOnInit() {
// ERREUR NG0203 : inject() hors contexte d'injection
const router = inject(Router);
}
}
// ✅ Bon usage : inject() dans le field initializer
@Component({ /* ... */ })
export class BonExempleComponent {
// Déclaré ici = contexte valide (initialisé avant le constructeur)
private router = inject(Router);
ngOnInit() {
// On utilise la propriété, pas inject() directement
this.router.navigate(['/tableau-de-bord']);
}
}
inject() dans une méthode ou un callback. Déplacez toujours l'appel à inject() dans un field initializer ou dans le constructeur.
inject() dans les composants standalone
Les composants standalone: true d'Angular 17+ sont le terrain naturel de inject(). Sans module NgModule pour déclarer les providers, inject() simplifie encore davantage la configuration.
Exemple complet : composant de messagerie
import { Component, signal, inject, computed } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
@Component({
selector: 'app-messagerie',
standalone: true,
// Imports déclarés directement sur le composant (pas de NgModule)
imports: [FormsModule],
template: `
<div class="container py-4">
<h2>Messagerie — {{ messagesNonLus() }} non lu(s)</h2>
<div class="input-group mb-3">
<input
class="form-control"
placeholder="Rechercher un message..."
[(ngModel)]="recherche"
>
</div>
@for (msg of messagesFiltres(); track msg.id) {
<div class="card mb-2" [class.border-primary]="!msg.lu">
<div class="card-body py-2">
<div class="d-flex justify-content-between">
<strong>{{ msg.expediteur }}</strong>
<small class="text-muted">{{ msg.date }}</small>
</div>
<p class="mb-0">{{ msg.sujet }}</p>
</div>
</div>
}
<button class="btn btn-outline-secondary mt-3" (click)="retourTableauDeBord()">
← Retour
</button>
</div>
`
})
export class MessagerieComponent {
// inject() remplace entièrement le constructeur
private router = inject(Router);
private messagerieService = inject(MessagerieService);
// Signals locaux au composant
recherche = signal('');
messages = signal(this.messagerieService.chargerMessages());
// computed() dérive automatiquement des deux signals
messagesFiltres = computed(() => {
const terme = this.recherche().toLowerCase();
if (!terme) return this.messages();
return this.messages().filter(m =>
m.sujet.toLowerCase().includes(terme) ||
m.expediteur.toLowerCase().includes(terme)
);
});
messagesNonLus = computed(() =>
this.messages().filter(m => !m.lu).length
);
retourTableauDeBord() {
this.router.navigate(['/tableau-de-bord']);
}
}
Injection dans les directives standalone
inject() fonctionne aussi dans les directives — très utile pour créer des comportements réutilisables :
// Directive standalone qui ajoute un tooltip Bootstrap
import { Directive, ElementRef, inject, Input, OnInit } from '@angular/core';
@Directive({
selector: '[appTooltip]',
standalone: true
})
export class TooltipDirective implements OnInit {
// inject(ElementRef) accède à l'élément DOM hôte
private el = inject(ElementRef);
@Input('appTooltip') texte = '';
@Input() position: 'top' | 'bottom' | 'left' | 'right' = 'top';
ngOnInit() {
// Configurer les attributs Bootstrap tooltip sur l'élément
this.el.nativeElement.setAttribute('data-bs-toggle', 'tooltip');
this.el.nativeElement.setAttribute('data-bs-placement', this.position);
this.el.nativeElement.setAttribute('title', this.texte);
}
}
// Utilisation dans un composant :
// <button [appTooltip]="'Supprimer ce fichier'" position="bottom">
// <i class="bi bi-trash"></i>
// </button>
inject() dans les guards et interceptors
Les guards et interceptors fonctionnels (apparus avec Angular 15) sont l'une des utilisations les plus élégantes d'inject(). Au lieu d'une classe avec un constructeur, vous écrivez une simple fonction.
Guard fonctionnel avec inject()
// guards/auth.guard.ts
import { inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { AuthService } from '../services/auth.service';
// Une fonction pure — pas de classe, pas de constructeur
export const authGuard: CanActivateFn = (route, state) => {
// inject() fonctionne ici car les guards s'exécutent dans un contexte d'injection
const auth = inject(AuthService);
const router = inject(Router);
if (auth.estConnecte()) {
return true; // Accès autorisé
}
// Rediriger vers login avec l'URL de retour
return router.createUrlTree(['/connexion'], {
queryParams: { retour: state.url }
});
};
// Dans app.routes.ts :
// { path: 'tableau-de-bord', component: TDBComponent, canActivate: [authGuard] }
Guard de rôle paramétrable
// Guard qui vérifie un rôle spécifique — retourne une factory de guard
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { PermissionsService } from '../services/permissions.service';
export function roleGuard(roleRequis: string): CanActivateFn {
return () => {
const permissions = inject(PermissionsService);
const router = inject(Router);
if (permissions.aLeRole(roleRequis)) {
return true;
}
// Rediriger vers une page "accès refusé"
return router.createUrlTree(['/acces-refuse'], {
queryParams: { role: roleRequis }
});
};
}
// Utilisation :
// { path: 'admin', component: AdminComponent, canActivate: [roleGuard('ADMIN')] }
// { path: 'editeur', component: EditeurComponent, canActivate: [roleGuard('EDITEUR')] }
Interceptor HTTP fonctionnel
// interceptors/auth.interceptor.ts
import { inject } from '@angular/core';
import { HttpInterceptorFn } from '@angular/common/http';
import { AuthService } from '../services/auth.service';
// Interceptor fonctionnel : une simple fonction, pas de classe
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
const token = auth.obtenirToken();
if (!token) {
// Aucun token : laisser passer la requête telle quelle
return next(req);
}
// Cloner la requête et ajouter le header Authorization
const requeteAuthentifiee = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
'X-App-Version': '2.0'
}
});
return next(requeteAuthentifiee);
};
// Dans main.ts ou app.config.ts :
// provideHttpClient(withInterceptors([authInterceptor]))
inject() est la seule façon d'accéder aux services Angular dans ce contexte. C'est précisément pour ça que la fonction inject() a été conçue.
Créer des composables avec inject()
Un composable est une fonction TypeScript ordinaire qui appelle inject() à l'intérieur. Elle encapsule de la logique réutilisable avec ses dépendances. C'est le pattern le plus puissant rendu possible par inject().
Composable de pagination
// composables/use-pagination.ts
import { signal, computed, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
interface OptionsPagination {
elementsParPage?: number;
}
// La fonction appelle inject() — utilisable dans n'importe quel composant
export function usePagination(options: OptionsPagination = {}) {
const router = inject(Router);
const route = inject(ActivatedRoute);
const elementsParPage = options.elementsParPage ?? 10;
// Page courante lue depuis l'URL (query param ?page=X)
const pageActuelle = signal(
Number(route.snapshot.queryParamMap.get('page') ?? 1)
);
// Calculer l'offset pour l'API
const offset = computed(() => (pageActuelle() - 1) * elementsParPage);
function allerPage(page: number) {
pageActuelle.set(page);
// Synchroniser l'URL avec la page
router.navigate([], {
relativeTo: route,
queryParams: { page },
queryParamsHandling: 'merge' // conserver les autres query params
});
}
function pagePrecedente() { allerPage(Math.max(1, pageActuelle() - 1)); }
function pageSuivante(total: number) {
const dernierePage = Math.ceil(total / elementsParPage);
allerPage(Math.min(dernierePage, pageActuelle() + 1));
}
return { pageActuelle, offset, allerPage, pagePrecedente, pageSuivante, elementsParPage };
}
// Utilisation dans n'importe quel composant — zéro import de service
@Component({
selector: 'app-liste-commandes',
standalone: true,
template: `
<!-- Liste des commandes -->
@for (cmd of commandes(); track cmd.id) {
<div class="border-bottom py-2">{{ cmd.reference }} — {{ cmd.montant }}€</div>
}
<!-- Navigation pagination -->
<nav class="d-flex gap-2 mt-3" aria-label="Pagination">
<button class="btn btn-sm btn-outline-secondary"
(click)="pagination.pagePrecedente()"
[disabled]="pagination.pageActuelle() === 1">
← Précédent
</button>
<span class="d-flex align-items-center px-3">
Page {{ pagination.pageActuelle() }}
</span>
<button class="btn btn-sm btn-outline-secondary"
(click)="pagination.pageSuivante(totalCommandes())">
Suivant →
</button>
</nav>
`
})
export class ListeCommandesComponent {
private commandesService = inject(CommandesService);
// Le composable encapsule Router + ActivatedRoute + logique de pagination
pagination = usePagination({ elementsParPage: 15 });
totalCommandes = signal(0);
commandes = resource({
request: () => this.pagination.offset(),
loader: async ({ request: offset }) => {
const data = await fetch(`/api/commandes?offset=${offset}&limit=15`).then(r => r.json());
this.totalCommandes.set(data.total);
return data.items;
}
}).value;
}
Composable de confirmation
// composables/use-confirmation.ts — dialog de confirmation réutilisable
import { inject, signal } from '@angular/core';
import { ToastService } from '../services/toast.service';
export function useConfirmation() {
const toast = inject(ToastService);
const enAttente = signal(false);
async function confirmer(message: string, action: () => Promise<void>) {
// Utiliser window.confirm — en production : remplacer par un modal Bootstrap
if (!window.confirm(message)) return;
enAttente.set(true);
try {
await action();
toast.succes('Action effectuée avec succès');
} catch (e) {
toast.erreur('Une erreur est survenue');
} finally {
enAttente.set(false);
}
}
return { enAttente, confirmer };
}
// Utilisation :
// const confirmation = useConfirmation();
// confirmation.confirmer('Supprimer ce fichier ?', () => fichierService.supprimer(id));
InjectionToken : injecter des valeurs
L'injection de dépendances ne se limite pas aux services. Avec InjectionToken, vous pouvez injecter des valeurs de configuration, des constantes, des fonctions ou n'importe quel objet.
Créer et utiliser un InjectionToken
// tokens/config.token.ts
import { InjectionToken } from '@angular/core';
// Définir l'interface de configuration
interface ConfigApp {
apiUrl: string;
langue: 'fr' | 'en' | 'es';
debugMode: boolean;
versionsMax: number;
}
// Créer le token avec un nom lisible (pour le débogage Angular DevTools)
export const CONFIG_APP = new InjectionToken<ConfigApp>('CONFIG_APP');
// app.config.ts — fournir la valeur au démarrage
import { ApplicationConfig } from '@angular/core';
import { CONFIG_APP } from './tokens/config.token';
export const appConfig: ApplicationConfig = {
providers: [
{
provide: CONFIG_APP,
useValue: {
apiUrl: 'https://api.monapp.fr/v2',
langue: 'fr',
debugMode: false,
versionsMax: 10
}
}
]
};
// Utiliser inject() avec le token dans n'importe quel composant/service
import { inject } from '@angular/core';
import { CONFIG_APP } from '../tokens/config.token';
@Component({ /* ... */ })
export class HeaderComponent {
// Injection du token — TypeScript connaît le type grâce au générique
private config = inject(CONFIG_APP);
// Accès direct aux propriétés typées
get apiUrl() { return this.config.apiUrl; }
get debugMode(){ return this.config.debugMode; }
}
Token avec valeur par défaut via factory
// Token qui calcule sa valeur au démarrage
import { InjectionToken, PLATFORM_ID, isPlatformBrowser } from '@angular/core';
// Factory : calculer si on est dans un navigateur
export const EST_NAVIGATEUR = new InjectionToken<boolean>('EST_NAVIGATEUR', {
providedIn: 'root',
// factory() s'exécute dans un contexte d'injection → inject() est valide ici
factory: () => isPlatformBrowser(inject(PLATFORM_ID))
});
// Dans un composant SSR (Angular Universal / NgOptimizedImage) :
@Component({ /* ... */ })
export class AnimationComponent {
private estNavigateur = inject(EST_NAVIGATEUR);
ngOnInit() {
if (this.estNavigateur) {
// Lancer les animations seulement dans le navigateur
this.demarrerAnimations();
}
}
}
export const API_URL = '...') est fixe à la compilation. Un InjectionToken peut être surchargé dans les tests avec une valeur différente via TestBed.configureTestingModule({ providers: [{ provide: CONFIG_APP, useValue: configTest }] }). C'est ce qui rend votre code testable.
Options avancées : optional, self, skipSelf
Le deuxième argument d'inject() accepte des options qui contrôlent comment Angular cherche le provider dans la hiérarchie des injecteurs.
optional : injection facultative
// Service de feature flag — optionnel selon l'environnement
@Component({ /* ... */ })
export class BoutonAchatComponent {
private analytics = inject(AnalyticsService, { optional: true });
// ↑ retourne null si non fourni
acheter(produitId: number) {
// Utiliser le service seulement s'il est disponible
this.analytics?.track('purchase_intent', { produitId });
// ... logique d'achat
}
}
self : chercher seulement dans l'injecteur local
// Directive qui vérifie si elle est dans un contexte FormGroup
import { Directive, inject } from '@angular/core';
import { ControlContainer, FormGroupDirective } from '@angular/forms';
@Directive({ selector: '[appValidationLocale]', standalone: true })
export class ValidationLocaleDirective {
// self: true → chercher seulement dans le FormGroup immédiatement parent
// Sans self, Angular remonterait dans toute la hiérarchie
private formGroup = inject(FormGroupDirective, { self: true, optional: true });
ngOnInit() {
if (!this.formGroup) {
console.warn('appValidationLocale doit être utilisé dans un FormGroup');
}
}
}
skipSelf : ignorer l'injecteur courant
// Service qui a besoin d'accéder au service parent (pas soi-même)
@Injectable()
export class SousDossierService {
// skipSelf: chercher le LoggerService dans les injecteurs PARENTS
// Evite la récursion infinie si ce service fournit aussi LoggerService
private loggerParent = inject(LoggerService, { skipSelf: true, optional: true });
}
| Option | Effet | Cas d'usage |
|---|---|---|
optional: true |
Retourne null si non trouvé (pas d'erreur) |
Services optionnels, feature flags, analytics |
self: true |
Cherche uniquement dans l'injecteur du composant courant | Directives qui vérifient leur contexte immédiat |
skipSelf: true |
Ignore l'injecteur courant, cherche dans les parents | Éviter la récursion, accéder aux services parents |
host: true |
Remonte jusqu'à l'injecteur de l'élément hôte | Directives qui ont besoin du composant hôte |
Migration et bonnes pratiques
Migrer progressivement depuis le constructeur
Vous n'avez pas besoin de tout migrer d'un coup. Les deux approches coexistent dans un même projet. Voici une stratégie de migration par étapes :
// Étape 1 — Départ : constructeur classique
@Component({ /* ... */ })
export class ArticlesComponent implements OnInit {
articles: Article[] = [];
constructor(
private articlesService: ArticlesService,
private route: ActivatedRoute
) {}
ngOnInit() {
this.articlesService.charger().subscribe(data => this.articles = data);
}
}
// Étape 2 — Migration : inject() + suppression du constructeur
@Component({ /* ... */ })
export class ArticlesComponent {
// Les services injectés en propriétés — constructeur vide supprimé
private articlesService = inject(ArticlesService);
private route = inject(ActivatedRoute);
articles = resource({
loader: () => this.articlesService.charger()
});
// ngOnInit n'est plus nécessaire
}
Règles à suivre absolument
- Toujours déclarer
inject()en propriétés de classe, jamais dans les méthodes - Préférer
privatepour les injections internes au composant - Nommer les propriétés injectées de façon explicite (
articlesServiceet nonsvc) - Extraire la logique complexe dans des composables, pas dans des méthodes de composant
- Utiliser
{ optional: true }pour les dépendances non critiques - Préférer
InjectionTokenaux constantes globales pour tout ce qui doit être testable - Ne pas mélanger constructeur + inject() pour les mêmes services — choisir un style
Tester un composant avec inject()
// Les composants avec inject() sont aussi faciles à tester qu'avec le constructeur
import { TestBed } from '@angular/core/testing';
import { ProfilComponent } from './profil.component';
import { UtilisateurService } from './utilisateur.service';
describe('ProfilComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ProfilComponent], // standalone = direct import
providers: [
{
provide: UtilisateurService,
// Mock du service — inject() récupérera ce mock automatiquement
useValue: {
chargerProfil: () => Promise.resolve({ nom: 'Alice', email: 'alice@test.fr' })
}
}
]
});
});
it('affiche le nom de l\'utilisateur', async () => {
const fixture = TestBed.createComponent(ProfilComponent);
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.textContent).toContain('Alice');
});
});
inject() n'est pas une révolution — c'est une évolution. Il simplifie l'écriture des composants, rend possible les composables réutilisables, et s'impose comme le standard Angular 19+ pour l'injection de dépendances. Commencez par l'utiliser dans vos nouveaux composants standalone, et migrez progressivement les anciens quand l'occasion se présente.