Chapter 7: Services and Dependency Injection
So far, our components have managed their own data and logic. But what if we need to share functionality across components? For example:
- A logging service used by multiple features.
- A data service to fetch users from an API.
- A state service to hold global settings.
Putting all this logic inside components would make them messy and hard to reuse. Angular’s solution is services, combined with a powerful dependency injection system.
7.1 What Are Services?
A service is a class that encapsulates logic, data, or functionality that you want to share across components. Services are not tied to the DOM; instead, they handle business logic.
Example: a simple logging service.
export class LoggerService {
log(message: string) {
console.log('[LOG]:', message);
}
}
This service can now be reused anywhere in your application.
7.2 The @Injectable Decorator
To make a service available for Angular’s DI system, you mark it with @Injectable().
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root' // service is available app-wide
})
export class LoggerService {
log(message: string) {
console.log('[LOG]:', message);
}
}
Here:
@Injectable()tells Angular this class can participate in DI.providedIn: 'root'makes the service a singleton (one instance shared across the whole app).
7.3 Using a Service in a Component
The most common way is constructor injection.
import { Component } from '@angular/core';
import { LoggerService } from './logger.service';
@Component({
selector: 'app-dashboard',
standalone: true,
template: `<button (click)="logMessage()">Log</button>`
})
export class DashboardComponent {
constructor(private logger: LoggerService) {}
logMessage() {
this.logger.log('Dashboard button clicked!');
}
}
Angular sees the LoggerService type in the constructor, finds it in the injector tree, and provides an instance.
7.4 Hierarchical Injectors
Angular uses a hierarchical injector system:
- Root injector – created when the app starts (services with
providedIn: 'root'). - Component injectors – each component can have its own injector.
This means you can scope services differently.
Example: Service Scoped to a Component
@Component({
selector: 'app-user-list',
standalone: true,
providers: [LoggerService], // new instance for this component
template: `
<button (click)="logMessage()">Log</button>
`
})
export class UserListComponent {
constructor(private logger: LoggerService) {}
logMessage() {
this.logger.log('UserList button clicked!');
}
}
Here, every UserListComponent gets its own instance of LoggerService.
7.5 Singleton Services and Scopes
- Singleton: If you provide a service in the
rootinjector, the same instance is reused everywhere. - Scoped: If you provide a service at a component level, Angular creates a new instance for that component (and its children).
- Feature-scoped: If you provide a service in a lazy-loaded module, Angular creates a new instance for that module.
This flexibility allows you to balance shared state and isolation.
7.6 Constructor Injection vs. @inject
Traditionally, Angular used constructor injection:
constructor(private logger: LoggerService) {}
But starting with Angular v14+, you can also use the new inject() function inside your class:
import { Component, inject } from '@angular/core';
import { LoggerService } from './logger.service';
@Component({
selector: 'app-settings',
standalone: true,
template: `<button (click)="logMessage()">Log</button>`
})
export class SettingsComponent {
private logger = inject(LoggerService);
logMessage() {
this.logger.log('Settings updated!');
}
}
When to Use Each
- Constructor injection: still the default, especially when injecting multiple services (cleaner and explicit).
inject()function: useful in- standalone components (avoids clutter in constructor),
- utility classes (where no constructor exists),
- signals/effects or static methods.
Example: Using inject() in a signal-based service:
import { Injectable, signal, inject } from '@angular/core';
import { LoggerService } from './logger.service';
@Injectable({ providedIn: 'root' })
export class CounterService {
private logger = inject(LoggerService);
count = signal(0);
increment() {
this.count.update(c => c + 1);
this.logger.log(`Count is now ${this.count()}`);
}
}
7.7 Best Practices
- Prefer providedIn: 'root' for global services.
- Use component-level providers for state that should reset with the component.
- Don’t overload services with UI logic — keep them focused on business logic.
- Use constructor injection for clarity, but
inject()for advanced cases. - Avoid creating circular dependencies (service A depends on service B and vice versa).
7.8 Summary
- Services encapsulate shared logic and data.
@Injectablemakes them available to Angular’s DI system.- Angular injectors are hierarchical, supporting global, feature, and component-level scoping.
- Services can be singleton or scoped depending on where they are provided.
- Angular now supports both constructor injection and the new
inject()function, giving you more flexibility.