Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 root injector, 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.
  • @Injectable makes 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.