Skip to content

Singleton Services and Shared State

When a service is provided at the root level (providedIn: 'root'), Angular creates exactly one instance for the entire application. Every component that injects it receives a reference to the same object. This is a singleton.

Consider WatchlistService. Multiple components need to know the current watchlist:

  • NavBar shows a badge with the watchlist count
  • MovieCard shows a “Saved” indicator for movies that are in the list
  • Watchlist page renders all saved movies

If each component maintained its own copy of the watchlist, they would fall out of sync. Adding a movie in MovieCard would not update NavBar’s badge. Removing one in Watchlist would not update the MovieCard indicators.

With a singleton service, all three components read from the same array. Any mutation is immediately visible everywhere.

Angular signals (introduced in Angular 16) are the cleanest way to hold reactive state in a service. A signal is a value that notifies its readers when it changes:

watchlist.service.ts
import { Injectable, signal, computed } from '@angular/core';
import { Movie } from '../models/movie.model';
@Injectable({ providedIn: 'root' })
export class WatchlistService {
private movies = signal<Movie[]>(this.load());
readonly items = this.movies.asReadonly();
readonly count = computed(() => this.movies().length);
has(id: number): boolean {
return this.movies().some(m => m.id === id);
}
toggle(movie: Movie): void {
if (this.has(movie.id)) {
this.remove(movie.id);
} else {
this.add(movie);
}
}
private add(movie: Movie): void {
this.movies.update(list => [...list, movie]);
this.save();
}
private remove(id: number): void {
this.movies.update(list => list.filter(m => m.id !== id));
this.save();
}
private save(): void {
localStorage.setItem('watchlist', JSON.stringify(this.movies()));
}
private load(): Movie[] {
try {
return JSON.parse(localStorage.getItem('watchlist') ?? '[]');
} catch {
return [];
}
}
}

Key details:

  • signal<Movie[]>() holds the array. The initial value is loaded from localStorage.
  • asReadonly() exposes a read-only version of the signal — components can read but not write.
  • computed(() => this.movies().length) derives the count from the movies signal. It updates automatically when the movies change.
  • update() applies a function to the current value and sets the result — the immutable pattern for arrays.

Components and templates read signals by calling them as functions:

nav-bar.html
@if (watchlistService.count() > 0) {
<span class="badge">{{ watchlistService.count() }}</span>
}
nav-bar.ts
watchlistService = inject(WatchlistService);

Angular tracks which signals a template reads. When a signal changes, Angular re-renders only the components whose templates read it.

When MovieCard calls this.watchlistService.toggle(movie):

  1. WatchlistService.movies signal is updated
  2. Angular detects the change
  3. Every component whose template reads count() or has() re-renders
  4. NavBar’s badge updates, MovieCard’s “Saved” indicator updates, Watchlist page updates

All from one service, one signal, zero coordination code in components.

  1. Create a CounterService with @Injectable({ providedIn: 'root' }).
  2. Add a count = signal(0) property.
  3. Add increment(), decrement(), and reset() methods.
  4. Create two components that both inject CounterService.
  5. Show the count in both components. Add increment/decrement buttons to one.
  6. Confirm that clicking buttons in one component updates the count displayed in the other.
  • Root-level services are singletons — one instance shared across the entire app.
  • Signals (signal<T>()) hold reactive state that notifies readers when it changes.
  • asReadonly() prevents components from writing to a signal directly — they must call service methods.
  • computed(() => expr) derives values from signals and updates automatically.
  • update(fn) applies a transformation to the current signal value — use it for immutable array updates.
  • When a signal changes, Angular re-renders only the components whose templates read that signal.