Services and Dependency Injection

Chapter 6
Services and Dependency Injection
Sharing logic and state across components without passing it through every layer — Angular's most distinctive feature

The React course needed Context (Intermediate Chapter 2) and eventually Zustand/Redux (Advanced Chapter 1) to share state across distant components without prop drilling. Angular has a single, built-in answer baked into the framework from the start: a service — an ordinary class holding shared logic or state — combined with dependency injection (DI), which hands that service to any component that asks for it. This is arguably Angular's defining feature.

A Service Is Just a Class

// counter.service.ts import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class CounterService { private count = 0; increment() { this.count++; } getCount() { return this.count; } }

The @Injectable({ providedIn: 'root' }) decorator marks this class as something Angular's DI system can provide, and 'root' means a single shared instance exists for the entire app — every component that asks for CounterService gets that same one instance, making it a natural place to hold shared state. (A service can also be provided at a narrower scope, but app-wide 'root' is the common default.)

Injecting a Service Into a Component

// counter.component.ts import { Component, inject } from '@angular/core'; import { CounterService } from './counter.service'; @Component({ selector: 'app-counter', standalone: true, template: ` <p>Count: {{ counter.getCount() }}</p> <button (click)="counter.increment()">+1</button> `, }) export class CounterComponent { counter = inject(CounterService); }

inject(CounterService) asks Angular's DI system for the service — the component never creates it with new CounterService() itself. That distinction is the whole point of dependency injection: the component declares what it needs, and the framework supplies it, automatically handing over the same shared 'root' instance. Any other component injecting CounterService shares the exact same count, with no props, no Context provider, and nothing passed between them.

inject() vs constructor injection
Older Angular code requests services through the constructor instead: constructor(private counter: CounterService) {}. Both achieve identical results — the newer inject() function (used throughout this course) reads more cleanly and works in more places, but recognize the constructor form when you encounter it in existing codebases; it remains fully supported.

Why This Replaces Prop Drilling and Context

In React, sharing one piece of state between a header badge and a distant cart page required lifting state up and then either prop drilling or wrapping the tree in a Context provider (the exact journey the Shopping Cart project took). In Angular, both components simply inject() the same service — there's no provider to wrap anything in, no value object to memoize (Intermediate Chapter 7's concern), and no tree structure that the shared state has to flow through. The service exists independently of the component tree entirely.

Services for Logic, Not Just State

// logger.service.ts — a stateless service, purely for shared behavior @Injectable({ providedIn: 'root' }) export class LoggerService { log(message: string) { console.log(`[${new Date().toISOString()}] ${message}`); } }

A service doesn't have to hold state — it's equally the home for shared behavior: logging, formatting, calculations, and (most importantly) talking to a backend API, which the HTTP client chapter builds on directly. Keeping that logic in an injectable service rather than inside components is a core Angular convention: components handle the view, services handle everything else.

A plain getCount() in a template won't always reflect async changes cleanly
Calling a method like counter.getCount() directly in a template works for state changed synchronously by user events (Angular re-checks the template after each event). But for state that changes asynchronously — a timer, an API response — exposing the value as an Observable and using the async pipe (Chapters 7 and 11), or Angular's newer signals (Chapter 13), is the more robust pattern. The plain-method approach here is the simplest starting point, deliberately, before those tools are introduced.
Sharing needReact approachAngular approach
Nearby componentsLift state up + propsA shared service (or @Input/@Output)
Distant componentsContext, or Zustand/ReduxA service injected into both
Shared behavior/API callsA custom hook / util moduleAn injectable service

Coding Challenges

Challenge 1

Build a CounterService (providedIn: 'root') holding a count with increment/decrement/reset methods, and inject it into a single component that displays and changes the count via buttons.

📄 View solution
Challenge 2

Inject the same CounterService into two completely separate, unrelated components — one that increments the count and one that only displays it — confirming both reflect the same shared value with nothing passed between them.

📄 View solution
Challenge 3

Build a stateless LoggerService with a log(message) method that prefixes a timestamp, and inject it into a component that calls it from a button click.

📄 View solution

Chapter 6 Quick Reference

  • A service — an ordinary class for shared state or behavior, marked @Injectable({ providedIn: 'root' })
  • providedIn: 'root' — one shared instance for the whole app
  • inject(ServiceClass) — asks Angular's DI to supply the service; never new it yourself
  • Two distant components injecting the same service share its state — no provider, no prop drilling, no Context
  • Services are also the home for shared behavior: logging, formatting, and (next) API calls
  • Older code injects via the constructor (constructor(private x: X)) — equivalent to inject()
  • Next chapter: pipes — transforming displayed values in the template