Unde pui un provider decide câte instanțe există și cât trăiesc. Angular are o ierarhie de injectori: când ceri ceva cu inject(), căutarea începe de la elementul curent și urcă până găsește un provider. Asta îți permite să ai un singleton global, o instanță per feature sau o instanță per componentă — cu același cod de serviciu.
Ierarhia
┌──────────────────────────────┐
│ Platform injector │ (rar: providedIn: 'platform')
└──────────────┬───────────────┘
┌──────────────▼───────────────┐
│ Root (EnvironmentInjector) │ @Service(), providedIn: 'root', app.config.ts
└──────────────┬───────────────┘
┌──────────────▼───────────────┐
│ Route injectors │ { path, providers: [...] } — pentru rute lazy
└──────────────┬───────────────┘
┌──────────────▼───────────────┐
│ Element injectors │ @Component({ providers, viewProviders })
│ (urmează arborele DOM) │
└──────────────────────────────┘
inject(X) caută: elementul curent → părinții din DOM → route injector → root → platform → eroare NG0201Cele trei niveluri pe care le folosești zilnic
// 1. ROOT — o instanță pentru toată aplicația
@Service()
export class AuthStore {}
// 2. RUTĂ — o instanță pentru un feature (tot ce e sub /admin)
export const routes: Routes = [
{
path: 'admin',
providers: [AdminStore, { provide: API_URL, useValue: '/api/admin' }],
loadChildren: () => import('./admin/admin.routes'),
},
];
// 3. COMPONENTĂ — o instanță per componentă (și copiii ei)
@Component({
selector: 'app-checkout',
providers: [CheckoutState], // fiecare <app-checkout> are starea lui
})
export class Checkout {}#10#10Doi copii, un singur provider → împart instanța.
#20Alt provider → altă instanță, stare separată.
#3#4nullself, skipSelf, optional.
| Unde | Instanțe | Durată de viață | Exemple |
|---|---|---|---|
| root | 1 | toată aplicația | auth, temă, coș, cache, HttpClient |
| rută | 1 per rută (lazy) | de la prima activare (+ cleanup opțional v22.2) | store de feature, config de feature |
| componentă | 1 per instanță de componentă | cât componenta | stare de wizard, formular, editor, tabel |
Rețete de provideri
providers: [
// useClass — altă implementare pentru același token
{ provide: Logger, useClass: environment.production ? RemoteLogger : ConsoleLogger },
// useValue — o valoare fixă (config, mock)
{ provide: API_URL, useValue: 'https://api.example.com' },
// useFactory — calculată, cu acces la inject()
{ provide: Storage, useFactory: () => (inject(PLATFORM_ID) === 'browser' ? localStorage : new MemoryStorage()) },
// useExisting — alias: același obiect sub alt token
{ provide: AbstractCart, useExisting: CartStore },
// multi — mai mulți provideri adunați într-un array
{ provide: VALIDATORS, useValue: requiredValidator, multi: true },
{ provide: VALIDATORS, useValue: emailValidator, multi: true },
];Modificatori de căutare
inject(Counter, { optional: true }); // null în loc de eroare dacă nu există
inject(Counter, { self: true }); // doar din injectorul elementului curent
inject(Counter, { skipSelf: true }); // sari peste elementul curent, caută la părinți
inject(Counter, { host: true }); // oprește căutarea la componenta gazdă
// Exemplu real: o directivă care se poate „agăța” de un părinte opțional
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;
}Tiparul de folosit: skipSelf găsește părintele, iar optional transformă „nu există” în null pentru meniul de sus. Adâncimea rezultă de la sine.
providers vs viewProviders
// providers: vizibili în template-ul componentei ȘI în conținutul proiectat (ng-content)
// viewProviders: vizibili DOAR în template-ul propriu, NU și în conținutul proiectat
@Component({
selector: 'app-tabs',
viewProviders: [TabsInternalState], // copiii proiectați de părinte nu-l pot injecta
template: `<ng-content />`,
})
export class Tabs {}Curățarea injectorilor de rută (v22.2)
// Implicit, injectorii de rută trăiesc cât aplicația (o dată creați, rămân).
// v22.2: withAutoCleanupInjectors() îi distruge când ruta nu mai e activă
// (cu RouteReuseStrategy.shouldDestroyInjector care returnează true pentru rutele dorite).
provideRouter(routes, withAutoCleanupInjectors());Cum alegi nivelul
- Starea trebuie să supraviețuiască navigării (utilizator, coș, preferințe).
- E infrastructură fără stare de UI (API client, logger).
- E folosit în multe feature-uri.
- Starea trebuie să dispară când pleci din pagină (filtre, formular, wizard).
- Poți avea mai multe instanțe simultan (două editoare deschise, două tabele).
- Configurare diferită pe zone (alt
API_URLpentru admin).
Cum declari un serviciu e la inject() & @Service(); valorile care nu sunt clase (configurație, feature flags) se furnizează cu InjectionToken & provideX().
Capcane frecvente
- Același serviciu în
providersla mai multe niveluri → mai multe instanțe fără să vrei („de ce nu se sincronizează coșul?”). - Serviciu root care injectează un serviciu de componentă → NG0201: root nu vede injectorii de element.
providerspe componente dintr-o listă mare → o instanță per rând. Ok dacă e intenționat, scump dacă nu.- Route providers nu sunt distruși implicit la navigare — nu te baza pe ei pentru „reset la ieșire” fără
withAutoCleanupInjectors().