Ein Service ist eine Klasse, die gemeinsame Logik oder gemeinsamen Zustand hält und per Dependency Injection in die Komponenten gelangt. In v22 kommt der Decorator @Service() hinzu: eine kürzere und klarere Variante für den 95-%-Fall — ein Singleton, das in der gesamten Anwendung verfügbar ist. Und inject() ist der empfohlene Weg, Abhängigkeiten anzufordern.
@Service() — neu in v22v22 stabil
import { Service, computed, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Service() // v22: entspricht @Injectable({ providedIn: 'root' })
export class CartService {
private readonly http = inject(HttpClient); // ✓ @Service verlangt inject() — kein DI im Konstruktor
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]);
}
}| Früher | Heute (v22) |
|---|---|
@Injectable({ providedIn: 'root' }) | @Service() |
@Injectable() + providers: [X] | @Service({ autoProvided: false }) + providers: [X] |
@Injectable({ providedIn: 'root', useFactory }) | @Service({ factory: () => ... }) |
| DI über den Konstruktor erlaubt | nur inject() (sonst Fehler NG2028) |
Optionen
// 1. Standard: ein Singleton in root, beim ersten Injizieren erzeugt, Tree-shakable
@Service()
export class Analytics {}
// 2. Kein Auto-Provide: Er muss explizit in `providers` eingetragen werden (eine Instanz pro Komponente / Route)
@Service({ autoProvided: false })
export class WizardState {}
@Component({ providers: [WizardState] /* ... */ })
export class CheckoutWizard {}
// 3. Mit Factory: Du steuerst, wie er erzeugt wird (Konfiguration, Wahl der Implementierung)
@Service({ factory: () => new ApiClient(inject(API_URL)) })
export class ApiClient {
constructor(readonly baseUrl: string) {} // wird von deiner Factory aufgerufen, nicht von DI
}Abstrakte Klasse + Standardimplementierung
// Pattern: eine abstrakte Klasse = Vertrag + Standardimplementierung (v22 erlaubt eine Factory auf abstrakten Klassen)
@Service({ factory: () => new ConsoleNotifier() })
export abstract class Notifier {
abstract notify(message: string): void;
}
class ConsoleNotifier extends Notifier {
notify(message: string) { console.info(message); }
}
@Injectable() // Unterklassen, die mit useClass verwendet werden, brauchen einen eigenen Decorator
class ToastNotifier extends Notifier {
private readonly toasts = inject(ToastService);
notify(message: string) { this.toasts.show(message); }
}
// Überall: inject(Notifier) → ConsoleNotifier
// In einem Bereich der Anwendung (oder in Tests) tauschst du die Implementierung aus:
@Component({ providers: [{ provide: Notifier, useClass: ToastNotifier }] })
export class AdminShell {}1. Derselbe Root-Service in zwei verschiedenen Komponenten → dieselbe Instanz
inject(VisitCounter)
Instanz
#2 · count: 0inject(VisitCounter)
Instanz
#2 · count: 02. Eine abstrakte Klasse mit Standard-Factory, lokal überschrieben
ConsoleNotifier (default)
BannerNotifier (overridden in providers)
@Service akzeptiert kein DI über den Konstruktor
// ✗ Fehler 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. Konstruktor
// Warum inject() statt Konstruktor?
// 1. Vererbung ohne super(...) mit allen Abhängigkeiten
export abstract class BasePage {
protected readonly route = inject(ActivatedRoute);
protected readonly title = inject(Title);
}
export class UsersPage extends BasePage {
private readonly users = inject(UsersApi); // kein constructor(route, title, users) { super(route, title) }
}
// 2. Es funktioniert in Funktionen (Guards, Resolver, Interceptors, Factories)
export const authGuard: CanActivateFn = () => inject(AuthStore).isLoggedIn();
// 3. Wiederverwendbare Funktionen, die injizieren („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');Der Injection-Kontext
// inject() funktioniert NUR in einem Injection-Kontext:
// ✓ der Konstruktor, Feldinitialisierung
// ✓ Provider-Factories (useFactory, InjectionToken-Factory)
// ✓ Guards, Resolver, Interceptors, Routen-Funktionen
// ✓ runInInjectionContext(injector, () => ...)
// ✗ ngOnInit, ein Click-Handler, setTimeout, subscribe → Fehler NG0203
export class Report {
private readonly injector = inject(Injector);
exportLater() {
setTimeout(() => {
runInInjectionContext(this.injector, () => {
const exporter = inject(PdfExporter); // ok
exporter.run();
});
});
}
}Wann ja, wann nein
Service anlegen
- Zustand, der zwischen Komponenten geteilt wird, die nicht in einer Parent-Child-Beziehung stehen (Warenkorb, aktueller Benutzer, Einstellungen).
- Zugriff auf eine API (eine Datenschicht pro Ressource/Feature).
- Wiederverwendbare Logik mit Abhängigkeiten (Formatierung mit LOCALE_ID, Berechtigungen, Logging).
- Integration mit Browser-APIs (Storage, Clipboard, Benachrichtigungen) — in Tests leicht zu mocken.
Keinen Service anlegen
- Reine Funktionen ohne Abhängigkeiten (
formatDate,slugify) → einfache exportierte Funktionen. - Zustand, den nur eine Komponente nutzt → Signals in der Komponente.
- Konstanten →
const, oder einInjectionToken, wenn sie konfigurierbar sein müssen. - Ein „Event-Service“ mit einem Subject für die Kommunikation zwischen Parent und Child → input/output.
Häufige Stolperfallen
@Service()+ ein Konstruktor mit Parametern → NG2028. Nutzeinject().- Ein Root-Service mit Feature-Zustand: Der Zustand überlebt die Navigation (z. B. bleiben die Filter einer Seite erhalten, nachdem du sie verlassen hast). Wenn du das nicht willst, stelle ihn auf Seiten- bzw. Routen-Ebene bereit (siehe Providers).
- Zirkuläre Abhängigkeiten (A injiziert B, B injiziert A) → Fehler NG0200. Meist muss ein Teil der Logik in einen dritten Service wandern.
inject()inngOnInit→ NG0203. Verschiebe es in ein Feld.