Angular Services & Dependency Injection
Services organize reusable logic and state outside of components.
Services & DI Essentials
- What: A service holds reusable logic/state. DI (dependency injection) supplies instances where needed.
- Scope: Provide in
root
for a shared singleton, or provide in a component for isolated instances. - Use cases: Data fetching, caching, business rules, cross-component state.
- Mental model: DI is like a power outlet: you plug in and Angular gives you a ready instance.
- Decorator: Use
@Injectable()
on classes that inject other services.
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CounterService { value = 0; inc() { this.value++; } }
// Inject in a component
// constructor(public counter: CounterService) {}
Notes:
- Related: See Components to consume services, Router for guards using services, and HTTP for services that fetch data.
- Use
@Injectable({ providedIn: 'root' })
for app-wide singletons. - Provide at a component when you need isolated instances.
- Use
inject()
in functions like route guards to retrieve dependencies outside constructors.
Service Basics
- Decorate classes with
@Injectable()
(required if they inject other services). - Inject services into constructors to use them in components.
- Use
providedIn: 'root'
for a shared singleton.
@Injectable({ providedIn: 'root' })
export class DataService {}
// class App { constructor(private data: DataService) {} }
Example
import { bootstrapApplication } from '@angular/platform-browser';
import { Component, Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CounterService {
value = 0;
inc() { this.value++; }
dec() { this.value--; }
reset() { this.value = 0; }
}
@Component({
selector: 'app-root',
standalone: true,
template: `
<h3>Services</h3>
<p>Counter: {{ counter.value }}</p>
<button (click)="counter.inc()">+</button>
<button (click)="counter.dec()">-</button>
<button (click)="counter.reset()">Reset</button>
`
})
export class App {
constructor(public counter: CounterService) {}
}
bootstrapApplication(App);
<app-root></app-root>
Example explained
@Injectable({ providedIn: 'root' })
: Registers a singleton service available app‑wide.- Inject in constructor:
constructor(public counter: CounterService)
exposes the service to the template. - Methods: Call
inc()
/dec()
/reset()
to update shared state.
Notes:
- Metadata required when injecting: If a service injects other services, add
@Injectable()
so DI can generate metadata. - Keep state minimal: Avoid large shared mutable state in services; Use explicit methods and return values.
- Standalone bootstrapping: Ensure providers are registered where needed and use
inject()
in functional constructs (e.g., guards).
Shared Service Across Components
providedIn: 'root'
shares one instance across the app.- Updating in one component reflects in others using the same service.
<counter-a></counter-a>
<counter-b></counter-b>
<!-- Both use the same service instance -->
Example
import { bootstrapApplication } from '@angular/platform-browser';
import { Component, Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CounterService {
value = 0;
inc() { this.value++; }
dec() { this.value--; }
}
@Component({
selector: 'counter-a',
standalone: true,
template: `
<h4>Counter A</h4>
<p>Value: {{ counter.value }}</p>
<button (click)="counter.inc()">+1</button>
<button (click)="counter.dec()">-1</button>
`
})
export class CounterA {
constructor(public counter: CounterService) {}
}
@Component({
selector: 'counter-b',
standalone: true,
template: `
<h4>Counter B</h4>
<p>Value: {{ counter.value }}</p>
<button (click)="counter.inc()">+1</button>
<button (click)="counter.dec()">-1</button>
`
})
export class CounterB {
constructor(public counter: CounterService) {}
}
@Component({
selector: 'app-root',
standalone: true,
imports: [CounterA, CounterB],
template: `
<h3>Shared Service Across Components</h3>
<counter-a></counter-a>
<counter-b></counter-b>
<p><em>Both components use the same CounterService instance.</em></p>
`
})
export class App {}
bootstrapApplication(App);
<app-root></app-root>
Example explained
providedIn: 'root'
shares one instance across the app.- Shared state: Clicking +/− in one component updates the value shown in the other.
- Template access: Injected as
public
so the template can readcounter.value
and call methods.
Notes:
- Singleton vs local:
providedIn: 'root'
yields a single shared instance; providing in a component creates separate instances per subtree. - Be deliberate about scope: Provide at the level that matches your sharing needs to avoid surprises.
Component-Provided Service (Hierarchical DI)
- Provide a service in a component's
providers
to create a local instance for its subtree. - Sibling subtrees receive separate instances.
@Component({ providers: [LocalCounterService] })
export class Panel {}
Example
import { bootstrapApplication } from '@angular/platform-browser';
import { Component, Injectable } from '@angular/core';
import { CommonModule } from '@angular/common';
@Injectable()
export class LocalCounterService {
id = Math.floor(Math.random() * 10000);
value = 0;
inc() { this.value++; }
}
@Component({
selector: 'counter-view',
standalone: true,
template: `
<p>Service #{{ svc.id }} value: {{ svc.value }}</p>
<button (click)="svc.inc()">+1</button>
`
})
export class CounterView {
constructor(public svc: LocalCounterService) {}
}
@Component({
selector: 'panel-a',
standalone: true,
imports: [CommonModule, CounterView],
providers: [LocalCounterService],
template: `
<h4>Panel A (own provider)</h4>
<counter-view></counter-view>
<counter-view></counter-view>
`
})
export class PanelA {}
@Component({
selector: 'panel-b',
standalone: true,
imports: [CommonModule, CounterView],
providers: [LocalCounterService],
template: `
<h4>Panel B (own provider)</h4>
<counter-view></counter-view>
<counter-view></counter-view>
`
})
export class PanelB {}
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, PanelA, PanelB],
styles: [`
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; }
h4 { margin: 0 0 8px; }
button { margin-top: 6px; }
`],
template: `
<h3>Component-Provided Service (Hierarchical DI)</h3>
<p>Each panel provides its own service instance.</p>
<p>Counters inside the same panel share the instance.</p>
<p>Different panels do not.</p>
<div class="grid">
<panel-a></panel-a>
<panel-b></panel-b>
</div>
`
})
export class App {}
bootstrapApplication(App);
<app-root></app-root>
Example explained
providers: [LocalCounterService]
: Each panel component gets its own service instance for its subtree.- Within a panel: Multiple
CounterView
children share the same local instance. - Between panels: Panel A and Panel B have different instances (see the different
id
values).
Notes:
- Multiple instances expected: Each provided component subtree gets its own instance; siblings do not share it.
- Avoid circular dependencies: Two services injecting each other will fail; extract common logic into a third service or redesign.
Service Design Tips
Keep services focused and easy to test:
- Avoid component coupling: Do not inject components into services; keep services UI-agnostic.
- Expose clear APIs: Use small methods returning plain values or Observables; keep internal state private.
- Configuration via tokens: Use injection tokens (a unique lookup key, often a class or an
InjectionToken
object) for configurable values to simplify testing and reuse. - Scope deliberately: Use
providedIn: 'root'
for app-wide singletons; provide at a component for isolated instances.