Modern Angular: Signals, Standalone Components and SSR · Components, Signals and Data

Services and Dependency Injection

Share state and logic through injectable services, configure them with injection tokens, scope instances with component providers, and replace them in tests.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A CartService (a root singleton with signal state) used by two components, a CURRENCY injection token with a default factory, a component that provides its own instance of a service, and a test setup that replaces a PricingApi with a fake.

The idea

  • @Injectable({ providedIn: "root" }) creates one app-wide instance, created lazily and tree-shakable.
  • inject(Token) gets a dependency in constructors, field initializers and factory functions.
  • InjectionToken<T> provides non-class values (configuration), optionally with a default factory.
  • providers: [Service] on a component creates an instance for that component and its children.
  • Tests and environments swap implementations with { provide: X, useValue / useClass / useFactory }.

The code

Root services, tokens, component providers and test overrides
import { Component, Injectable, InjectionToken, computed, inject, signal } from "@angular/core";
import { TestBed } from "@angular/core/testing";
import { BrowserTestingModule, platformBrowserTesting } from "@angular/platform-browser/testing";

export const CURRENCY = new InjectionToken<string>("CURRENCY", { factory: () => "USD" });

@Injectable({ providedIn: "root" })
class PricingApi {
  priceOf(item: string): number { throw new Error("calls the real pricing service"); }
}

@Injectable({ providedIn: "root" })
class CartService {
  private pricing = inject(PricingApi);
  private items = signal<string[]>([]);
  readonly count = computed(() => this.items().length);
  readonly total = computed(() => this.items().reduce((sum, item) => sum + this.pricing.priceOf(item), 0));
  add(item: string) { this.items.update((list) => [...list, item]); }
}

@Injectable()
class ViewCounter {                                   // no providedIn: provided per component
  views = 0;
}

@Component({
  selector: "app-product",
  providers: [ViewCounter],
  template: `<button (click)="add()">Add {{ name }}</button>`,
})
class ProductCard {
  name = "Teapot";
  private cart = inject(CartService);
  readonly counter = inject(ViewCounter);
  add() { this.cart.add(this.name); this.counter.views++; }
}

@Component({
  selector: "app-cart-badge",
  template: `<span>Cart: {{ cart.count() }} items, {{ cart.total() }} {{ currency }}</span>`,
})
class CartBadge {
  cart = inject(CartService);
  currency = inject(CURRENCY);
}

async function main() {
  TestBed.initTestEnvironment(BrowserTestingModule, platformBrowserTesting());
  TestBed.configureTestingModule({
    providers: [
      { provide: PricingApi, useValue: { priceOf: (item: string) => (item === "Teapot" ? 24 : 0) } },
      { provide: CURRENCY, useValue: "EUR" },
    ],
  });
  const first = TestBed.createComponent(ProductCard);
  const second = TestBed.createComponent(ProductCard);
  const badge = TestBed.createComponent(CartBadge);
  first.nativeElement.querySelector("button").click();
  first.nativeElement.querySelector("button").click();
  second.nativeElement.querySelector("button").click();
  await badge.whenStable();
  console.log(badge.nativeElement.textContent.trim());
  console.log("same CartService everywhere:", TestBed.inject(CartService) === badge.componentInstance.cart);
  console.log("ViewCounter per component:", first.componentInstance.counter.views, second.componentInstance.counter.views);
}
main();

Output

Cart: 3 items, 72 EUR
same CartService everywhere: true
ViewCounter per component: 2 1

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Providing a root service again in a component's providers, accidentally creating a second instance with separate state.
  • Using classes with only constants as "config services"; use an InjectionToken with a factory.
  • Calling inject() outside an injection context (for example inside a setTimeout callback).
  • Exposing writable signals from services; expose readonly/computed signals and methods that change state.

Key points

  • providedIn: "root" for app-wide singletons; component providers for scoped instances.
  • inject() and InjectionToken for classes and values.
  • Swap implementations with providers in tests and environments.

Further reading: Dependency injection · Creating an injectable service · Hierarchical injectors

Practice questions

What does providedIn: "root" mean?

Answer

The service is registered with the application's root injector: one shared instance, created on first use, and removed from the bundle if never injected.

What happens when a component lists a service in its providers?

Answer

That component (and its children) gets its own instance from the component's element injector, separate from any root instance.

When do you use an InjectionToken?

Answer

To inject values that aren't classes, such as configuration objects, strings or functions, optionally with a default factory.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?