L’endroit où vous placez un provider détermine combien d’instances existent et combien de temps elles vivent. Angular a une hiérarchie d’injecteurs : quand vous demandez quelque chose avec inject(), la recherche part de l’élément courant et remonte jusqu’à trouver un provider. Vous pouvez ainsi avoir un singleton global, une instance par fonctionnalité ou une instance par composant, avec le même code de service.
La hiérarchie
┌──────────────────────────────┐
│ Platform injector │ (rare : providedIn: 'platform')
└──────────────┬───────────────┘
┌──────────────▼───────────────┐
│ Root (EnvironmentInjector) │ @Service(), providedIn: 'root', app.config.ts
└──────────────┬───────────────┘
┌──────────────▼───────────────┐
│ Route injectors │ { path, providers: [...] } — pour les routes lazy
└──────────────┬───────────────┘
┌──────────────▼───────────────┐
│ Element injectors │ @Component({ providers, viewProviders })
│ (suit l’arbre DOM) │
└──────────────────────────────┘
inject(X) cherche dans : l’élément courant → ses parents DOM → l’injecteur de route → root → platform → erreur NG0201Les trois niveaux que vous utilisez tous les jours
// 1. ROOT — une instance pour toute l’application
@Service()
export class AuthStore {}
// 2. ROUTE — une instance pour une fonctionnalité (tout ce qui est sous /admin)
export const routes: Routes = [
{
path: 'admin',
providers: [AdminStore, { provide: API_URL, useValue: '/api/admin' }],
loadChildren: () => import('./admin/admin.routes'),
},
];
// 3. COMPOSANT — une instance par composant (et ses enfants)
@Component({
selector: 'app-checkout',
providers: [CheckoutState], // chaque <app-checkout> a son propre état
})
export class Checkout {}#90#90Deux enfants, un provider → ils partagent l’instance.
#100Un autre provider → une autre instance, un état séparé.
#11#12nullself, skipSelf, optional.
| Où | Instances | Durée de vie | Exemples |
|---|---|---|---|
| root | 1 | toute l’application | auth, thème, panier, cache, HttpClient |
| route | 1 par route (lazy) | dès la première activation (+ nettoyage optionnel en v22.2) | Store de fonctionnalité, configuration de fonctionnalité |
| composant | 1 par instance de composant | aussi longtemps que le composant | assistant, formulaire, éditeur, état de tableau |
Recettes de providers
providers: [
// useClass — une autre implémentation pour le même token
{ provide: Logger, useClass: environment.production ? RemoteLogger : ConsoleLogger },
// useValue — une valeur fixe (config, mock)
{ provide: API_URL, useValue: 'https://api.example.com' },
// useFactory — calculé, avec accès à inject()
{ provide: Storage, useFactory: () => (inject(PLATFORM_ID) === 'browser' ? localStorage : new MemoryStorage()) },
// useExisting — un alias : le même objet sous un autre token
{ provide: AbstractCart, useExisting: CartStore },
// multi — plusieurs providers rassemblés dans un tableau
{ provide: VALIDATORS, useValue: requiredValidator, multi: true },
{ provide: VALIDATORS, useValue: emailValidator, multi: true },
];Modificateurs de recherche
inject(Counter, { optional: true }); // null au lieu d’une erreur s’il n’existe pas
inject(Counter, { self: true }); // uniquement depuis l’injecteur de l’élément courant
inject(Counter, { skipSelf: true }); // ignore l’élément courant, cherche dans les parents
inject(Counter, { host: true }); // arrête la recherche au composant hôte
// Exemple concret : une directive qui peut se « brancher » sur un parent optionnel
export class MenuItem {
private readonly menu = inject(Menu, { optional: true, skipSelf: true });
}@Component({ selector: 'app-menu', template: '…<ng-content />' })
export class Menu {
readonly label = input.required<string>();
private readonly parent = inject(Menu, { skipSelf: true, optional: true });
readonly depth: number = (this.parent?.depth ?? 0) + 1;
}Le motif à utiliser : skipSelf trouve le parent, optional transforme « il n’y en a pas » en null pour le menu du haut. La profondeur en découle toute seule.
providers vs viewProviders
// providers : visible dans le template du composant ET dans le contenu projeté (ng-content)
// viewProviders : visible UNIQUEMENT dans son propre template, PAS dans le contenu projeté
@Component({
selector: 'app-tabs',
viewProviders: [TabsInternalState], // les enfants projetés par le parent ne peuvent pas l’injecter
template: `<ng-content />`,
})
export class Tabs {}Nettoyer les injecteurs de route (v22.2)
// Par défaut, les injecteurs de route vivent aussi longtemps que l’application (une fois créés, ils restent).
// v22.2 : withAutoCleanupInjectors() les détruit quand la route n’est plus active
// (avec RouteReuseStrategy.shouldDestroyInjector renvoyant true pour les routes voulues).
provideRouter(routes, withAutoCleanupInjectors());Comment choisir le niveau
- L’état doit survivre à la navigation (utilisateur, panier, préférences).
- C’est de l’infrastructure sans état d’UI (client d’API, logger).
- Il est utilisé dans de nombreuses fonctionnalités.
- L’état doit disparaître quand vous quittez la page (filtres, formulaire, assistant).
- Vous pouvez avoir besoin de plusieurs instances à la fois (deux éditeurs ouverts, deux tableaux).
- Configuration différente selon la zone (un autre
API_URLpour l’admin).
Déclarer un service : inject() et @Service() ; les valeurs qui ne sont pas des classes (configuration, feature flags) se fournissent avec InjectionToken et provideX().
Pièges fréquents
- Le même service dans
providersà plusieurs niveaux → plusieurs instances sans l’avoir voulu (« pourquoi le panier ne se synchronise pas ? »). - Un service root qui injecte un service de niveau composant → NG0201 : root ne voit pas les injecteurs d’élément.
providerssur des composants dans une grande liste → une instance par ligne. Correct si c’est voulu, coûteux sinon.- Les providers de route ne sont pas détruits par défaut à la navigation : ne comptez pas dessus pour un « reset en quittant » sans
withAutoCleanupInjectors().