Un service est une classe qui porte de la logique ou un état partagés et atteint les composants par Dependency Injection. En v22 apparaît le décorateur @Service() : une variante plus courte et plus claire pour le cas à 95 % — un singleton disponible dans toute l’application. Et inject() est la manière recommandée de demander des dépendances.
@Service() — nouveau en v22v22 stable
import { Service, computed, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Service() // v22 : équivalent de @Injectable({ providedIn: 'root' })
export class CartService {
private readonly http = inject(HttpClient); // ✓ @Service exige inject() — pas de DI dans le constructeur
private readonly _items = signal<CartItem[]>([]);
readonly items = this._items.asReadonly();
readonly total = computed(() => this._items().reduce((s, i) => s + i.price * i.qty, 0));
add(item: CartItem) {
this._items.update((list) => [...list, item]);
}
}| Avant | Maintenant (v22) |
|---|---|
@Injectable({ providedIn: 'root' }) | @Service() |
@Injectable() + providers: [X] | @Service({ autoProvided: false }) + providers: [X] |
@Injectable({ providedIn: 'root', useFactory }) | @Service({ factory: () => ... }) |
| DI par le constructeur autorisée | uniquement inject() (sinon erreur NG2028) |
Options
// 1. Par défaut : un singleton dans root, créé à la première injection, tree-shakable
@Service()
export class Analytics {}
// 2. Pas d’auto-provide : il faut l’ajouter explicitement dans `providers` (une instance par composant / route)
@Service({ autoProvided: false })
export class WizardState {}
@Component({ providers: [WizardState] /* ... */ })
export class CheckoutWizard {}
// 3. Avec une factory : vous contrôlez sa création (configuration, choix de l’implémentation)
@Service({ factory: () => new ApiClient(inject(API_URL)) })
export class ApiClient {
constructor(readonly baseUrl: string) {} // appelé par votre factory, pas par la DI
}Classe abstraite + implémentation par défaut
// Pattern : une classe abstraite = contrat + implémentation par défaut (v22 autorise une factory sur les classes abstraites)
@Service({ factory: () => new ConsoleNotifier() })
export abstract class Notifier {
abstract notify(message: string): void;
}
class ConsoleNotifier extends Notifier {
notify(message: string) { console.info(message); }
}
@Injectable() // les sous-classes utilisées avec useClass ont besoin de leur propre décorateur
class ToastNotifier extends Notifier {
private readonly toasts = inject(ToastService);
notify(message: string) { this.toasts.show(message); }
}
// Partout : inject(Notifier) → ConsoleNotifier
// Dans une zone de l’application (ou dans les tests), vous changez l’implémentation :
@Component({ providers: [{ provide: Notifier, useClass: ToastNotifier }] })
export class AdminShell {}1. Le même service root dans deux composants différents → la même instance
inject(VisitCounter)
instance
#3 · count: 0inject(VisitCounter)
instance
#3 · count: 02. Une classe abstraite avec une factory par défaut, surchargée localement
ConsoleNotifier (default)
BannerNotifier (overridden in providers)
@Service n’accepte pas la DI par le constructeur
// ✗ Erreur NG2028: a @Service class cannot use constructor dependency injection.
@Service()
export class Orders {
constructor(private http: HttpClient) {}
}
// ✓
@Service()
export class Orders {
private readonly http = inject(HttpClient);
}inject() vs constructeur
// Pourquoi inject() plutôt que le constructeur ?
// 1. Héritage sans super(...) avec toutes les dépendances
export abstract class BasePage {
protected readonly route = inject(ActivatedRoute);
protected readonly title = inject(Title);
}
export class UsersPage extends BasePage {
private readonly users = inject(UsersApi); // pas de constructor(route, title, users) { super(route, title) }
}
// 2. Fonctionne dans les fonctions (guards, resolvers, interceptors, factories)
export const authGuard: CanActivateFn = () => inject(AuthStore).isLoggedIn();
// 3. Fonctions réutilisables qui injectent (« composables »)
export function injectQueryParam(name: string) {
const route = inject(ActivatedRoute);
return toSignal(route.queryParamMap.pipe(map((p) => p.get(name))), { initialValue: null });
}
readonly search = injectQueryParam('q');Le contexte d’injection
// inject() fonctionne UNIQUEMENT dans un contexte d’injection :
// ✓ le constructeur, l’initialisation des champs
// ✓ les factories de provider (useFactory, factory d’InjectionToken)
// ✓ guards, resolvers, interceptors, fonctions de route
// ✓ runInInjectionContext(injector, () => ...)
// ✗ ngOnInit, un handler de clic, setTimeout, subscribe → erreur NG0203
export class Report {
private readonly injector = inject(Injector);
exportLater() {
setTimeout(() => {
runInInjectionContext(this.injector, () => {
const exporter = inject(PdfExporter); // ok
exporter.run();
});
});
}
}Quand OUI, quand NON
Créer un service
- État partagé entre des composants qui ne sont pas parent–enfant (panier, utilisateur courant, préférences).
- Accès à une API (une couche de données par ressource/feature).
- Logique réutilisable avec dépendances (formatage avec LOCALE_ID, permissions, logging).
- Intégration avec les API du navigateur (storage, presse-papiers, notifications) — facile à mocker dans les tests.
Ne pas créer de service
- Fonctions pures sans dépendances (
formatDate,slugify) → simples fonctions exportées. - État utilisé par un seul composant → signals dans le composant.
- Constantes →
const, ou unInjectionTokensi elles doivent être configurables. - Un « service d’événements » avec un Subject pour la communication parent–enfant → input/output.
Pièges fréquents
@Service()+ un constructeur avec des paramètres → NG2028. Utilisezinject().- Un service root avec un état de feature : l’état survit à la navigation (p. ex. les filtres d’une page restent quand vous la quittez). Si vous ne le souhaitez pas, fournissez-le au niveau de la page/route (voir Providers).
- Dépendances circulaires (A injecte B, B injecte A) → erreur NG0200. En général, une partie de la logique doit passer dans un troisième service.
inject()dansngOnInit→ NG0203. Déplacez-le dans un champ.