Un serviciu e o clasă care ține logică sau stare partajată și ajunge în componente prin dependency injection. În v22 apare decoratorul @Service(): o variantă mai scurtă și mai clară pentru cazul de 95% — un singleton disponibil în toată aplicația. Iar inject() e modul recomandat de a cere dependențe.
@Service() — nou în v22v22 stabil
import { Service, computed, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Service() // v22: echivalent cu @Injectable({ providedIn: 'root' })
export class CartService {
private readonly http = inject(HttpClient); // ✓ @Service cere inject() — fără DI în constructor
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]);
}
}| Înainte | Acum (v22) |
|---|---|
@Injectable({ providedIn: 'root' }) | @Service() |
@Injectable() + providers: [X] | @Service({ autoProvided: false }) + providers: [X] |
@Injectable({ providedIn: 'root', useFactory }) | @Service({ factory: () => ... }) |
| DI prin constructor permis | doar inject() (eroare NG2028 altfel) |
Opțiuni
// 1. Implicit: singleton în root, creat la prima injectare, tree-shakable
@Service()
export class Analytics {}
// 2. Fără auto-provide: trebuie pus explicit în `providers` (instanță per componentă / rută)
@Service({ autoProvided: false })
export class WizardState {}
@Component({ providers: [WizardState] /* ... */ })
export class CheckoutWizard {}
// 3. Cu factory: controlezi cum e creat (configurare, alegerea implementării)
@Service({ factory: () => new ApiClient(inject(API_URL)) })
export class ApiClient {
constructor(readonly baseUrl: string) {} // apelat de factory-ul tău, nu de DI
}Clasă abstractă + implementare implicită
// Pattern: clasă abstractă = contract + implementare implicită (v22 permite factory pe clase abstracte)
@Service({ factory: () => new ConsoleNotifier() })
export abstract class Notifier {
abstract notify(message: string): void;
}
class ConsoleNotifier extends Notifier {
notify(message: string) { console.info(message); }
}
@Injectable() // subclasele folosite cu useClass au nevoie de propriul decorator
class ToastNotifier extends Notifier {
private readonly toasts = inject(ToastService);
notify(message: string) { this.toasts.show(message); }
}
// Oriunde: inject(Notifier) → ConsoleNotifier
// Într-o zonă a aplicației (sau în teste) schimbi implementarea:
@Component({ providers: [{ provide: Notifier, useClass: ToastNotifier }] })
export class AdminShell {}1. Același serviciu root în două componente diferite → aceeași instanță
inject(VisitCounter)
instanța
#1 · count: 0inject(VisitCounter)
instanța
#1 · count: 02. Clasă abstractă cu factory implicit, suprascrisă local
ConsoleNotifier (default)
BannerNotifier (overridden in providers)
@Service nu acceptă DI prin constructor
// ✗ Eroare NG2028: @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 constructor
// De ce inject() în loc de constructor?
// 1. Moștenire fără super(...) cu toate dependențele
export abstract class BasePage {
protected readonly route = inject(ActivatedRoute);
protected readonly title = inject(Title);
}
export class UsersPage extends BasePage {
private readonly users = inject(UsersApi); // fără constructor(route, title, users) { super(route, title) }
}
// 2. Funcționează în funcții (guards, resolvers, interceptors, factory-uri)
export const authGuard: CanActivateFn = () => inject(AuthStore).isLoggedIn();
// 3. Funcții reutilizabile care injectează („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');Contextul de injecție
// inject() merge DOAR într-un context de injecție:
// ✓ constructor, inițializare de câmpuri
// ✓ factory-uri de provideri (useFactory, InjectionToken factory)
// ✓ guards, resolvers, interceptors, funcții de rută
// ✓ runInInjectionContext(injector, () => ...)
// ✗ ngOnInit, handler de click, setTimeout, subscribe → eroare NG0203
export class Report {
private readonly injector = inject(Injector);
exportLater() {
setTimeout(() => {
runInInjectionContext(this.injector, () => {
const exporter = inject(PdfExporter); // ok
exporter.run();
});
});
}
}Când DA, când NU
Fă un serviciu
- Stare partajată între componente care nu sunt părinte–copil (coș, utilizator curent, preferințe).
- Acces la API (un strat de date per resursă/feature).
- Logică reutilizabilă cu dependențe (formatare cu LOCALE_ID, permisiuni, logging).
- Integrare cu API-uri de browser (storage, clipboard, notificări) — ușor de mock-uit în teste.
Nu face serviciu
- Funcții pure fără dependențe (
formatDate,slugify) → simple funcții exportate. - Stare folosită doar de o componentă → semnale în componentă.
- Constante →
constsauInjectionTokendacă trebuie configurabile. - „Serviciu de evenimente” cu Subject pentru comunicare părinte–copil → input/output.
Capcane frecvente
@Service()+ constructor cu parametri → NG2028. Foloseșteinject().- Serviciu root cu stare de feature: starea persistă între navigări (ex. filtrele unei pagini rămân după ce pleci). Dacă nu vrei asta, furnizează-l la nivel de pagină/rută (vezi Providers).
- Dependențe circulare (A injectează B, B injectează A) → eroare NG0200. De obicei înseamnă că o parte din logică trebuie mutată într-un al treilea serviciu.
inject()înngOnInit→ NG0203. Mută-l în field.