MasterAngularComputedSignalswithaSmartMovie&SeriesLibrary

Master Angular Computed Signals with a Smart Movie & Series Library 🎬📺

Computed signals in Angular 19 are a game-changer. They allow you to define read-only derived state that reacts automatically to changes in its dependencies — no manual subscriptions or lifecycle trickery.

In this post, we'll build a Movie & Series Library app that uses:

  • signal() and computed() for reactive and derived state
  • ✅ Standalone Components
  • ✅ Angular Services for domain logic
  • ✅ Strongly typed TypeScript for safe, scalable architecture

What is computed() in Angular?

  • It's a read-only signal.
  • It recalculates automatically when its dependent signals change.
  • It memoizes (caches) the value unless dependencies are modified.
  • It allows a clean separation between raw data and derived data.

Use Case: Reactive Media Library

Features:

  • Manage a library of Movies and Series.
  • Dynamically compute:
    • Total count
    • Type-based filters
    • Favorites
  • Use computed() to keep everything reactive.

Step 1: Define Models

export interface MediaItem {
  id: number;
  title: string;
  type: 'movie' | 'series';
  favorite: boolean;
}

Step 2: MediaService with Signals and Computed

import { Injectable, signal, computed } from '@angular/core';
import { MediaItem } from './media.model';

@Injectable({ providedIn: 'root' })
export class MediaService {
  private media = signal<MediaItem[]>([
    { id: 1, title: 'Inception', type: 'movie', favorite: false },
    { id: 2, title: 'Breaking Bad', type: 'series', favorite: true },
    { id: 3, title: 'The Matrix', type: 'movie', favorite: true },
  ]);

  get all() {
    return this.media;
  }

  readonly count = computed(() => this.media().length);

  readonly movies = computed(() =>
    this.media().filter(m => m.type === 'movie')
  );

  readonly series = computed(() =>
    this.media().filter(m => m.type === 'series')
  );

  readonly favorites = computed(() =>
    this.media().filter(m => m.favorite)
  );

  toggleFavorite(id: number) {
    this.media.update(prev =>
      prev.map(item =>
        item.id === id ? { ...item, favorite: !item.favorite } : item
      )
    );
  }

  add(item: MediaItem) {
    this.media.update(prev => [...prev, item]);
  }
}

Step 3: Create Standalone Component

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { MediaService } from './media.service';

@Component({
  selector: 'app-media-library',
  standalone: true,
  imports: [CommonModule, FormsModule],
  template: `
    🎬 Media Library
    Total Items: {{ media.count() }}
    Movies: {{ media.movies().length }}
    Series: {{ media.series().length }}
    Favorites: {{ media.favorites().length }}

    Items
    
      
        {{ item.title }} ({{ item.type }})
        
          {{ item.favorite ? '★' : '☆' }}
        
      
    

    Add New Item
    
    
      Movie
      Series
    
    Add
  `
})
export class MediaLibraryComponent {
  title = '';
  type: 'movie' | 'series' = 'movie';

  constructor(public media: MediaService) {}

  add() {
    if (this.title.trim()) {
      this.media.add({
        id: Date.now(),
        title: this.title.trim(),
        type: this.type,
        favorite: false
      });
      this.title = '';
    }
  }
}

Final Thoughts

Feature Benefit
signal([]) Reactive source of truth
computed() Automatically derived state (cached too!)
Service pattern Clean domain logic separation
Standalone No NgModules, modern & minimal

Tips:

  • Avoid recalculating in templates — use computed() in services.
  • effect() pairs well when you want to trigger logic on changes.

Become a signal ninja. Rely on derived state. Your app will thank you.

Happy coding, and may your signals always stay consistent!💥