Dependency injection is how Angular apps share state, call APIs and stay testable.
Components should focus on presentation. Logic that is shared or not about the view (API calls, state, logging, formatting) goes into services: plain classes that Angular creates and hands out through dependency injection (DI).
In Angular 22 the CLI generates services with the `@Service()` decorator, which makes the class available application-wide as a singleton:
@Service()
export class CartStore { ... }You will also see the long-standing equivalent, @Injectable({ providedIn: 'root' }), in most existing code and libraries. Both are tree-shakable: if nothing injects the service, it isn't bundled.
To use a service, call `inject()` in a field initializer (or constructor):
export class CartBadge {
private cart = inject(CartStore);
}inject() works only in an injection context: during construction of a component, directive, pipe or service, or in provider factories, route guards and similar functions. Calling it later, in a click handler for example, throws.
Why DI instead of importing a module-level object? The framework controls lifetimes and scopes (one instance for the app, or one per component subtree), and tests can swap in fakes by providing a different implementation. Constructor parameter injection (constructor(private cart: CartStore)) is the older style and still works.
A service with signals is a simple, effective store: private writable signals, public read-only signals and computeds, and methods that update them.
Example
import { Component, Service, inject, signal, computed } from '@angular/core';
@Service() // app-wide singleton
export class CartStore {
private readonly _items = signal<string[]>([]);
readonly items = this._items.asReadonly(); // consumers can't write
readonly count = computed(() => this._items().length);
add(item: string) {
this._items.update((list) => [...list, item]);
}
}
@Component({
selector: 'app-product-button',
template: `<button (click)="cart.add('Book')">Add book</button>`,
})
export class ProductButton { protected cart = inject(CartStore); }
@Component({
selector: 'app-cart-badge',
template: `Cart: {{ cart.count() }}`, // updates when any button adds
})
export class CartBadge { protected cart = inject(CartStore); } // same instanceRemember
- Services hold shared logic and state; components inject them
@Service()(v22) or@Injectable({ providedIn: 'root' })makes an app-wide singleton- Call
inject()only in an injection context such as field initializers
Watch out for
- Calling
inject()inside an event handler orsetTimeout, outside an injection context.