Services and Dependency Injection
Ask "where does shared state live?" in React and you get a discussion. Ask in Angular and you get one word: a service. Cart state, the HTTP layer, logging, the current user - anything more than one component needs lives in a service class, and dependency injection (DI) delivers it. DI is the most enterprise-scented term in this guide and the actual idea is small; ten minutes here demystifies half of every Angular codebase you'll ever open.
A service is a class with a marker
// src/app/cart.service.ts
;
What just happened: a plain class holds signals and the methods that update them - phase 3's
reactivity, outside any component. @Injectable({ providedIn: 'root' }) registers it with the
injector: "root" means one instance for the whole application, created lazily the first time
someone asks. Note the shape: state private, a read-only computed exposed, mutations as named
methods - the service owns its invariants, and no component can reach in and corrupt the list.
inject(): asking for it
// header.ts
;
;
// product-page.ts
What just happened: both components asked the injector for CartService and received the same
instance. ProductPage.buy() updates the signal; Header's template - subscribed through
cart.count() - updates immediately. Shared state without a prop chain, an event bus, or a store
library: a singleton service plus signals is Angular's store pattern.
📝 Terminology: inject() must run in an injection context - field initializers and
constructors, practically speaking. Calling it later (in a click handler, a setTimeout) throws
NG0203. The pattern is always the same: grab dependencies at the top of the class, use them
anywhere. Legacy dialect: constructor parameters -
constructor(private cart: CartService) {} - same injector, older spelling, everywhere in
existing code.
Why the indirection earns its keep
You could import { cartService } from './cart' - a module singleton, like our Svelte guide's
.svelte.js pattern. Angular routes it through the injector for two reasons that matter at its
scale:
- Swappability without surgery. The injector is a lookup, and lookups can be overridden per
environment or per test: hand the test a
FakeCartServiceand the components under test never know. With hard imports, that seam doesn't exist - which is why "how do I mock this?" is a question Angular teams rarely ask. This is the testability argument that made DI famous, and it was Angular's core bet from version one. - Scoping when you need it.
providedIn: 'root'covers most services, but a component can provide its own instance (providers: [WizardState]in the decorator), giving each component subtree a private copy - the per-tree-instance case (two wizards, each with private step state) that pure module singletons can't express.
The trade, stated fairly: a layer of indirection you carry everywhere, for a seam you mostly exploit in tests and larger architectures. Angular decided the trade once, for everyone - very on brand.
Services beyond state
The same mechanism carries stateless concerns - and composes:
Layered services - the HTTP layer injected by data services injected by feature services - are the
skeleton of every large Angular app. Phase 6 adds the innermost layer: HttpClient, itself
delivered by DI.
Recap
- Shared state and logic live in
@Injectableservice classes;providedIn: 'root'= one lazy app-wide instance. inject(ServiceClass)in a field initializer delivers it; constructor-parameter injection is the legacy spelling of the same thing.- Singleton service + signals = Angular's built-in store: private state, exposed computeds, named mutations.
- DI's payoff is the seam - swap implementations in tests without touching consumers; its cost is indirection.
- Component-level
providersscope an instance to a subtree - the per-widget-tree case.
[
{
"q": "Header and ProductPage both inject(CartService) with providedIn: 'root'. ProductPage adds an item. Why does Header's badge update?",
"choices": [
"Angular broadcasts a change event between components that inject the same class",
"Both hold the same singleton instance, and the badge reads a computed over the instance's signal",
"inject() creates linked copies that sync automatically",
"The router refreshes all components on state change"
],
"answer": 1,
"why": [
"No events are involved - just one object and the signal graph.",
null,
"No copies exist - that's the point of root scope: one instance, delivered to all askers.",
"The router navigates; it plays no role in reactivity."
],
"explain": "providedIn: 'root' means one instance per app. Shared instance + signals = shared reactive state: any component's write flows to any component's read through the normal dependency graph."
},
{
"q": "Calling inject(CartService) inside a click handler throws NG0203. What's the rule?",
"choices": [
"Services can't be used from event handlers",
"inject() only works in an injection context - grab dependencies in field initializers or the constructor, then use them anywhere",
"The service was missing providedIn: 'root'",
"Click handlers run outside Angular's zone"
],
"answer": 1,
"why": [
"Using an already-injected service in a handler is the normal pattern - it's the injecting that's position-sensitive.",
null,
"A missing registration gives a no-provider error, not a context error.",
"Zones relate to change detection, not injection timing."
],
"explain": "The injector needs to know who is asking, which it only knows during construction. Inject at the top of the class (cart = inject(CartService)); call cart.add() from wherever you like."
},
{
"q": "What does routing dependencies through the injector buy over a plain import of a module singleton?",
"choices": [
"Faster instantiation",
"A replaceable seam - tests and environments can substitute implementations without touching consumer code - plus per-subtree scoping via component providers",
"Automatic persistence of service state",
"It's required for signals to work"
],
"answer": 1,
"why": [
"Instantiation cost is identical - lazy either way.",
null,
"Nothing persists unless you write it somewhere - services are in-memory objects.",
"Signals work anywhere; plenty of signal state lives outside services."
],
"explain": "DI's indirection is a lookup you can override: FakeCartService in tests, scoped instances per subtree in the app. That seam is the argument that justifies the ceremony."
}
]
← Phase 4: Component Inputs and Outputs · Guide overview · Phase 6: HTTP and Just Enough RxJS →
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 3 questions
1. Header and ProductPage both inject(CartService) with providedIn: 'root'. ProductPage adds an item. Why does Header's badge update?
2. Calling inject(CartService) inside a click handler throws NG0203. What's the rule?
3. What does routing dependencies through the injector buy over a plain import of a module singleton?