A service is a class that holds shared logic or state and reaches components through dependency injection. In v22 the @Service() decorator appears: a shorter and clearer variant for the 95% case — a singleton available across the whole application. And inject() is the recommended way to request dependencies.
@Service() — new in v22v22 stable
import { Service, computed, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Service() // v22: equivalent to @Injectable({ providedIn: 'root' })
export class CartService {
private readonly http = inject(HttpClient); // ✓ @Service requires inject() — no DI in the 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]);
}
}| Before | Now (v22) |
|---|---|
@Injectable({ providedIn: 'root' }) | @Service() |
@Injectable() + providers: [X] | @Service({ autoProvided: false }) + providers: [X] |
@Injectable({ providedIn: 'root', useFactory }) | @Service({ factory: () => ... }) |
| DI through the constructor allowed | only inject() (otherwise error NG2028) |
Options
// 1. Default: a singleton in root, created on first injection, tree-shakable
@Service()
export class Analytics {}
// 2. No auto-provide: it must be put explicitly in `providers` (an instance per component / route)
@Service({ autoProvided: false })
export class WizardState {}
@Component({ providers: [WizardState] /* ... */ })
export class CheckoutWizard {}
// 3. With a factory: you control how it is created (configuration, choosing the implementation)
@Service({ factory: () => new ApiClient(inject(API_URL)) })
export class ApiClient {
constructor(readonly baseUrl: string) {} // called by your factory, not by DI
}Abstract class + default implementation
// Pattern: an abstract class = contract + default implementation (v22 allows a factory on abstract classes)
@Service({ factory: () => new ConsoleNotifier() })
export abstract class Notifier {
abstract notify(message: string): void;
}
class ConsoleNotifier extends Notifier {
notify(message: string) { console.info(message); }
}
@Injectable() // subclasses used with useClass need their own decorator
class ToastNotifier extends Notifier {
private readonly toasts = inject(ToastService);
notify(message: string) { this.toasts.show(message); }
}
// Anywhere: inject(Notifier) → ConsoleNotifier
// In one area of the application (or in tests) you change the implementation:
@Component({ providers: [{ provide: Notifier, useClass: ToastNotifier }] })
export class AdminShell {}1. The same root service in two different components → the same instance
inject(VisitCounter)
instance
#4 · count: 0inject(VisitCounter)
instance
#4 · count: 02. An abstract class with a default factory, overridden locally
ConsoleNotifier (default)
BannerNotifier (overridden in providers)
@Service does not accept DI through the constructor
// ✗ Error 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 constructor
// Why inject() instead of the constructor?
// 1. Inheritance without super(...) with all the dependencies
export abstract class BasePage {
protected readonly route = inject(ActivatedRoute);
protected readonly title = inject(Title);
}
export class UsersPage extends BasePage {
private readonly users = inject(UsersApi); // no constructor(route, title, users) { super(route, title) }
}
// 2. It works in functions (guards, resolvers, interceptors, factories)
export const authGuard: CanActivateFn = () => inject(AuthStore).isLoggedIn();
// 3. Reusable functions that inject (“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');The injection context
// inject() works ONLY in an injection context:
// ✓ the constructor, field initialisation
// ✓ provider factories (useFactory, InjectionToken factory)
// ✓ guards, resolvers, interceptors, route functions
// ✓ runInInjectionContext(injector, () => ...)
// ✗ ngOnInit, a click handler, setTimeout, subscribe → error NG0203
export class Report {
private readonly injector = inject(Injector);
exportLater() {
setTimeout(() => {
runInInjectionContext(this.injector, () => {
const exporter = inject(PdfExporter); // ok
exporter.run();
});
});
}
}When YES, when NO
Make a service
- State shared between components that are not parent–child (cart, current user, preferences).
- Access to an API (a data layer per resource/feature).
- Reusable logic with dependencies (formatting with LOCALE_ID, permissions, logging).
- Integration with browser APIs (storage, clipboard, notifications) — easy to mock in tests.
Don’t make a service
- Pure functions without dependencies (
formatDate,slugify) → plain exported functions. - State used by a single component → signals in the component.
- Constants →
const, or anInjectionTokenif they must be configurable. - An “event service” with a Subject for parent–child communication → input/output.
Common pitfalls
@Service()+ a constructor with parameters → NG2028. Useinject().- A root service with feature state: the state survives navigation (e.g. a page’s filters stay after you leave). If you don’t want that, provide it at page/route level (see Providers).
- Circular dependencies (A injects B, B injects A) → error NG0200. Usually part of the logic must move into a third service.
inject()inngOnInit→ NG0203. Move it to a field.