Services and dependency injection

Angular · Essentials · 12 min

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 instance

Remember

  • 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 or setTimeout, outside an injection context.

Check yourself

  1. Two unrelated components both call inject(CartStore), which is provided in root. What do they get?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: angular.dev/guide/di

Services and dependency injection · Angular · Topolearn