Skip to main content

Angular Singleton Services

Introduction

In Angular applications, services are a fundamental building block used to organize and share business logic, data, or functionality across components. A singleton service is a service that has only one instance throughout the application's lifecycle. This pattern is particularly useful when you need to maintain shared state or provide functionality that should be consistent across your entire application.

Angular's dependency injection system is designed to create singleton services by default, which means when you inject a service into multiple components, they all receive the same instance of that service.

Understanding Singleton Services

What is a Singleton?

A singleton is a design pattern that ensures a class has only one instance while providing a global point of access to that instance. In Angular, this pattern is implemented through the dependency injection system.

Why Use Singleton Services?

Singleton services in Angular are useful for:

  1. Sharing data between components that don't have a direct parent-child relationship
  2. Centralizing application logic to avoid code duplication
  3. Maintaining application state consistently across components
  4. Managing API communications with backend services
  5. Coordinating actions across different parts of your application

Creating a Singleton Service

Let's create a simple counter service to demonstrate how singleton services work in Angular.

Step 1: Generate a Service

Using the Angular CLI, generate a new service:

bash
ng generate service services/counter

This creates two files:

  • counter.service.ts - The service class file
  • counter.service.spec.ts - A testing file for the service

Step 2: Implement the Service

Open the counter.service.ts file and add the following code:

typescript
import { Injectable } from '@angular/core';

@Injectable({
providedIn: 'root'
})
export class CounterService {
private count = 0;

constructor() {
console.log('CounterService instance created');
}

increment(): void {
this.count++;
}

decrement(): void {
this.count--;
}

getCount(): number {
return this.count;
}

reset(): void {
this.count = 0;
}
}

Let's break down what's happening here:

  • The @Injectable() decorator marks the class as available to be provided and injected as a dependency.
  • providedIn: 'root' specifies that Angular should provide the service in the root injector, making it available throughout the application as a singleton.
  • We've defined methods to increment, decrement, get, and reset a counter value.
  • The console.log statement will help us verify that only one instance is created.

Using a Singleton Service

Now let's create components that will use our singleton service.

Step 1: Create Components

Generate two separate components:

bash
ng generate component components/counter-one
ng generate component components/counter-two

Step 2: Inject and Use the Service

Modify the counter-one.component.ts file:

typescript
import { Component } from '@angular/core';
import { CounterService } from '../../services/counter.service';

@Component({
selector: 'app-counter-one',
template: `
<div class="counter-component">
<h2>Counter One</h2>
<p>Count: {{ getCount() }}</p>
<button (click)="increment()">Increment</button>
<button (click)="decrement()">Decrement</button>
</div>
`,
styles: [`
.counter-component {
padding: 20px;
border: 1px solid #ddd;
margin-bottom: 20px;
}
`]
})
export class CounterOneComponent {
constructor(private counterService: CounterService) { }

increment(): void {
this.counterService.increment();
}

decrement(): void {
this.counterService.decrement();
}

getCount(): number {
return this.counterService.getCount();
}
}

Similarly, modify the counter-two.component.ts file:

typescript
import { Component } from '@angular/core';
import { CounterService } from '../../services/counter.service';

@Component({
selector: 'app-counter-two',
template: `
<div class="counter-component">
<h2>Counter Two</h2>
<p>Count: {{ getCount() }}</p>
<button (click)="increment()">Increment</button>
<button (click)="decrement()">Decrement</button>
<button (click)="reset()">Reset</button>
</div>
`,
styles: [`
.counter-component {
padding: 20px;
border: 1px solid #ddd;
margin-bottom: 20px;
}
`]
})
export class CounterTwoComponent {
constructor(private counterService: CounterService) { }

increment(): void {
this.counterService.increment();
}

decrement(): void {
this.counterService.decrement();
}

reset(): void {
this.counterService.reset();
}

getCount(): number {
return this.counterService.getCount();
}
}

Step 3: Add Components to App Component

Update your app.component.html to include both components:

html
<div class="container">
<h1>Angular Singleton Service Demo</h1>
<div class="components-container">
<app-counter-one></app-counter-one>
<app-counter-two></app-counter-two>
</div>
</div>

Step 4: Test the Application

When you run the application and open the browser console, you should see the message "CounterService instance created" only once, confirming that a single instance is shared.

If you increment the counter in "Counter One" and then check "Counter Two", you'll see that they share the same count because they use the same service instance.

Understanding Providedln Options

The @Injectable() decorator accepts a metadata object that determines how the service should be provided:

1. providedIn: 'root'

typescript
@Injectable({
providedIn: 'root'
})

This is the most common approach and creates a singleton service available throughout the application. Angular optimizes this by:

  • Creating the service only when it's first injected
  • Including the service in the final bundle only if it's used

2. providedIn: SomeModule

typescript
@Injectable({
providedIn: SomeModule
})

This creates a singleton within the context of a specific module. All components within the module share the same instance.

3. No providedIn

typescript
@Injectable()

When no providedIn is specified, you need to manually provide the service in a module's providers array:

typescript
@NgModule({
declarations: [/* components */],
imports: [/* modules */],
providers: [CounterService],
})
export class AppModule { }

Testing Singleton Services

To demonstrate that our service is indeed a singleton, let's create a simple test:

typescript
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { CounterService } from './services/counter.service';

@Component({
selector: 'app-root',
template: `
<div class="container">
<h1>Angular Singleton Service Demo</h1>
<div class="components-container">
<app-counter-one></app-counter-one>
<app-counter-two></app-counter-two>
<button (click)="checkServiceIdentity()">Check Service Identity</button>
<p *ngIf="isSame !== null">
Are service instances the same? {{ isSame ? 'YES' : 'NO' }}
</p>
</div>
</div>
`
})
export class AppComponent implements OnInit {
private counterService1: CounterService;
private counterService2: CounterService;
isSame: boolean | null = null;

constructor(private counterService: CounterService) {
this.counterService1 = counterService;
}

ngOnInit() {
// Injecting the service again to verify singleton behavior
this.counterService2 = this.counterService;
}

checkServiceIdentity() {
this.isSame = this.counterService1 === this.counterService2;
console.log('Are service instances the same?', this.isSame);
}
}

When you click the "Check Service Identity" button, it will compare the two instances and confirm they are the same object.

Common Use Cases for Singleton Services

1. Authentication Service

A classic example is an authentication service that manages user login state across your application:

typescript
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

interface User {
id: number;
username: string;
email: string;
}

@Injectable({
providedIn: 'root'
})
export class AuthService {
private currentUserSubject = new BehaviorSubject<User | null>(null);
public currentUser: Observable<User | null> = this.currentUserSubject.asObservable();

constructor() {
// Check localStorage for saved user on initialization
const savedUser = localStorage.getItem('currentUser');
if (savedUser) {
this.currentUserSubject.next(JSON.parse(savedUser));
}
}

login(username: string, password: string): boolean {
// In a real app, this would be an API call
if (username === 'admin' && password === 'password123') {
const user: User = {
id: 1,
username: 'admin',
email: '[email protected]'
};

// Store user details in localStorage
localStorage.setItem('currentUser', JSON.stringify(user));
this.currentUserSubject.next(user);
return true;
}
return false;
}

logout(): void {
// Remove user from local storage and set current user to null
localStorage.removeItem('currentUser');
this.currentUserSubject.next(null);
}

isLoggedIn(): boolean {
return this.currentUserSubject.value !== null;
}

getCurrentUser(): User | null {
return this.currentUserSubject.value;
}
}

2. Theme Service

A service to manage application themes and preferences:

typescript
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

export type Theme = 'light' | 'dark' | 'system';

@Injectable({
providedIn: 'root'
})
export class ThemeService {
private themeSubject = new BehaviorSubject<Theme>('system');
public theme$: Observable<Theme> = this.themeSubject.asObservable();

constructor() {
// Load saved theme from localStorage
const savedTheme = localStorage.getItem('theme') as Theme;
if (savedTheme) {
this.themeSubject.next(savedTheme);
this.applyTheme(savedTheme);
} else {
this.applyTheme('system');
}
}

setTheme(theme: Theme): void {
localStorage.setItem('theme', theme);
this.themeSubject.next(theme);
this.applyTheme(theme);
}

private applyTheme(theme: Theme): void {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
let isDark = theme === 'dark' || (theme === 'system' && prefersDark);

document.body.classList.remove('light-theme', 'dark-theme');
document.body.classList.add(isDark ? 'dark-theme' : 'light-theme');
}
}

3. Notification Service

A service to manage toast notifications and alerts:

typescript
import { Injectable } from '@angular/core';
import { Subject, Observable } from 'rxjs';

export interface Notification {
id: number;
message: string;
type: 'success' | 'error' | 'info' | 'warning';
duration?: number;
}

@Injectable({
providedIn: 'root'
})
export class NotificationService {
private notificationsSubject = new Subject<Notification>();
private notificationId = 0;

notifications$: Observable<Notification> = this.notificationsSubject.asObservable();

success(message: string, duration: number = 3000): void {
this.show({
id: this.getNextId(),
message,
type: 'success',
duration
});
}

error(message: string, duration: number = 5000): void {
this.show({
id: this.getNextId(),
message,
type: 'error',
duration
});
}

info(message: string, duration: number = 3000): void {
this.show({
id: this.getNextId(),
message,
type: 'info',
duration
});
}

warning(message: string, duration: number = 4000): void {
this.show({
id: this.getNextId(),
message,
type: 'warning',
duration
});
}

private show(notification: Notification): void {
this.notificationsSubject.next(notification);
}

private getNextId(): number {
return this.notificationId++;
}
}

Avoiding Common Pitfalls

1. Unintended Multiple Instances

While Angular creates singleton services by default, there are ways to inadvertently create multiple instances:

typescript
@Component({
selector: 'app-some-component',
templateUrl: './some.component.html',
providers: [CounterService] // ❌ This creates a new instance for this component!
})
export class SomeComponent { }

By including a service in a component's providers array, you create a new instance specific to that component and its children, breaking the singleton pattern.

2. Module Providers

Similarly, if you provide the service in multiple module providers arrays, you may create multiple instances:

typescript
@NgModule({
declarations: [/* components */],
imports: [/* modules */],
providers: [CounterService], // Be careful with this in lazy-loaded modules
})
export class FeatureModule { }

To maintain a singleton across lazy-loaded modules, use providedIn: 'root' instead.

3. Service-in-Service Dependency

Services can depend on other services. To inject a service into another service:

typescript
@Injectable({
providedIn: 'root'
})
export class DataService {
constructor(private http: HttpClient, private authService: AuthService) { }

fetchData() {
// Use authService to get auth token
const token = this.authService.getToken();

// Use token in HTTP request
return this.http.get('/api/data', {
headers: {
Authorization: `Bearer ${token}`
}
});
}
}

Summary

Singleton services in Angular provide a powerful way to share state and functionality across your application. By default, Angular's dependency injection system creates singleton services when you specify providedIn: 'root' or when you register services in the root module's providers array.

Key takeaways:

  1. Singleton services have only one instance throughout the application
  2. They're perfect for sharing data and functionality across components
  3. Angular uses dependency injection to manage service instances
  4. The @Injectable({ providedIn: 'root' }) syntax is the recommended way to create application-wide singleton services
  5. Be careful not to accidentally create multiple instances by providing services in component or feature module providers

Additional Resources

Exercises

  1. Basic Counter Service: Extend the counter service to include methods for incremental increases (e.g., by 5, 10) and add a method to return the count history.

  2. Shopping Cart Service: Create a singleton service that manages a shopping cart state, including methods to add items, remove items, calculate the total, and persist the cart to localStorage.

  3. Language Translation Service: Implement a singleton service that manages translations for your application, allowing components to request translated text for different languages.

  4. Service Communication: Create two different services that communicate with each other through dependency injection, where one service depends on functionality provided by the other.

  5. Service with Environment Config: Create a service that provides different functionality based on the environment (development vs. production) using Angular's environment files.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)