Wo du einen Provider platzierst, entscheidet, wie viele Instanzen existieren und wie lange sie leben. Angular hat eine Hierarchie von Injectors: Wenn du etwas mit inject() anforderst, beginnt die Suche beim aktuellen Element und steigt nach oben, bis ein Provider gefunden wird. So kannst du einen globalen Singleton, eine Instanz pro Feature oder eine Instanz pro Komponente haben — mit demselben Service-Code.
Die Hierarchie
┌──────────────────────────────┐
│ Platform injector │ (selten: providedIn: 'platform')
└──────────────┬───────────────┘
┌──────────────▼───────────────┐
│ Root (EnvironmentInjector) │ @Service(), providedIn: 'root', app.config.ts
└──────────────┬───────────────┘
┌──────────────▼───────────────┐
│ Route injectors │ { path, providers: [...] } — für Lazy-Routen
└──────────────┬───────────────┘
┌──────────────▼───────────────┐
│ Element injectors │ @Component({ providers, viewProviders })
│ (folgt dem DOM-Baum) │
└──────────────────────────────┘
inject(X) sucht in: aktuelles Element → DOM-Parents → Route-Injector → root → platform → Fehler NG0201Die drei Ebenen, die du täglich nutzt
// 1. ROOT — eine Instanz für die ganze Anwendung
@Service()
export class AuthStore {}
// 2. ROUTE — eine Instanz für ein Feature (alles unter /admin)
export const routes: Routes = [
{
path: 'admin',
providers: [AdminStore, { provide: API_URL, useValue: '/api/admin' }],
loadChildren: () => import('./admin/admin.routes'),
},
];
// 3. KOMPONENTE — eine Instanz pro Komponente (und ihre Kinder)
@Component({
selector: 'app-checkout',
providers: [CheckoutState], // jedes <app-checkout> hat seinen eigenen Zustand
})
export class Checkout {}#50#50Zwei Kinder, ein Provider → sie teilen sich die Instanz.
#60Anderer Provider → andere Instanz, getrennter Zustand.
#7#8nullself, skipSelf, optional.
| Wo | Instanzen | Lebensdauer | Beispiele |
|---|---|---|---|
| root | 1 | die ganze Anwendung | Auth, Theme, Warenkorb, Cache, HttpClient |
| Route | 1 pro Route (lazy) | ab der ersten Aktivierung (+ optionales Cleanup in v22.2) | Feature-Store, Feature-Konfiguration |
| Komponente | 1 pro Komponenteninstanz | so lange wie die Komponente | Wizard, Formular, Editor, Tabellenzustand |
Provider-Rezepte
providers: [
// useClass — eine andere Implementierung für denselben Token
{ provide: Logger, useClass: environment.production ? RemoteLogger : ConsoleLogger },
// useValue — ein fester Wert (Config, Mock)
{ provide: API_URL, useValue: 'https://api.example.com' },
// useFactory — berechnet, mit Zugriff auf inject()
{ provide: Storage, useFactory: () => (inject(PLATFORM_ID) === 'browser' ? localStorage : new MemoryStorage()) },
// useExisting — ein Alias: dasselbe Objekt unter einem anderen Token
{ provide: AbstractCart, useExisting: CartStore },
// multi — mehrere Provider, die in einem Array gesammelt werden
{ provide: VALIDATORS, useValue: requiredValidator, multi: true },
{ provide: VALIDATORS, useValue: emailValidator, multi: true },
];Lookup-Modifier
inject(Counter, { optional: true }); // null statt eines Fehlers, wenn er nicht existiert
inject(Counter, { self: true }); // nur aus dem Injector des aktuellen Elements
inject(Counter, { skipSelf: true }); // aktuelles Element überspringen, in den Parents suchen
inject(Counter, { host: true }); // beendet die Suche an der Host-Komponente
// Praxisbeispiel: eine Direktive, die sich an einen optionalen Parent „anhängen“ kann
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;
}Das zu verwendende Muster: skipSelf findet das Elternmenü, optional macht aus „gibt es nicht“ ein null für das oberste Menü. Die Tiefe ergibt sich von selbst.
providers vs. viewProviders
// providers: sichtbar im Template der Komponente UND im projizierten Inhalt (ng-content)
// viewProviders: sichtbar NUR im eigenen Template, NICHT im projizierten Inhalt
@Component({
selector: 'app-tabs',
viewProviders: [TabsInternalState], // vom Parent projizierte Kinder können ihn nicht injizieren
template: `<ng-content />`,
})
export class Tabs {}Route-Injectors aufräumen (v22.2)
// Standardmäßig leben Route-Injectors so lange wie die Anwendung (einmal erzeugt, bleiben sie).
// v22.2: withAutoCleanupInjectors() zerstört sie, wenn die Route nicht mehr aktiv ist
// (wobei RouteReuseStrategy.shouldDestroyInjector für die gewünschten Routen true zurückgibt).
provideRouter(routes, withAutoCleanupInjectors());So wählst du die Ebene
- Der Zustand muss die Navigation überleben (Benutzer, Warenkorb, Einstellungen).
- Es ist Infrastruktur ohne UI-Zustand (API-Client, Logger).
- Er wird in vielen Features genutzt.
- Der Zustand muss verschwinden, wenn du die Seite verlässt (Filter, Formular, Wizard).
- Du brauchst eventuell mehrere Instanzen gleichzeitig (zwei Editoren, zwei Tabellen).
- Unterschiedliche Konfiguration pro Bereich (ein anderes
API_URLfür Admin).
Wie du einen Service deklarierst, zeigt inject() & @Service(); Werte, die keine Klassen sind (Konfiguration, Feature-Flags), stellst du mit InjectionToken & provideX() bereit.
Häufige Stolperfallen
- Derselbe Service in
providersauf mehreren Ebenen → mehrere Instanzen, ohne es zu wollen („Warum synchronisiert sich der Warenkorb nicht?“). - Ein Root-Service, der einen Service auf Komponentenebene injiziert → NG0201: root sieht keine Element-Injectors.
providersauf Komponenten in einer großen Liste → eine Instanz pro Zeile. In Ordnung, wenn gewollt, teuer, wenn nicht.- Route-Provider werden bei der Navigation standardmäßig nicht zerstört — verlass dich nicht auf sie für ein „Zurücksetzen beim Verlassen“ ohne
withAutoCleanupInjectors().