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.
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 defaultfactory.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
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
InjectionTokenwith a factory. - Calling
inject()outside an injection context (for example inside asetTimeoutcallback). - Exposing writable signals from services; expose
readonly/computedsignals 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.