In Part 1, we mastered components and templates. Now in Part 2 of the Mastering Angular series, we go deeper into Signals — the reactive primitive that is fundamentally changing how Angular apps manage state.

The Problem Signals Solve

Before Signals, Angular relied on Zone.js to detect changes. Zone.js monkey-patches browser APIs and triggers change detection after almost any async operation. This works, but it can cause:

  • Unnecessary re-renders across the entire component tree
  • Hard-to-debug “change detection loops”
  • Poor performance in large apps with deep component trees

Signals solve this by making reactivity explicit and granular.

Creating & Reading Signals

A signal is a reactive container for a value. You call it like a function to read its current value.

import { signal, computed, effect } from '@angular/core';

const temperature = signal(22); // Celsius

// Read the value — returns 22
console.log(temperature());

// Update the value
temperature.set(25);

// Update based on current value
temperature.update(t => t + 1); // 26

Computed Signals

Computed signals derive their value from one or more other signals. They are lazy (calculated on demand) and memoized (only recalculated when dependencies change).

const celsius = signal(22);

const fahrenheit = computed(() => (celsius() * 9/5) + 32);
const description = computed(() => {
  const temp = celsius();
  if (temp < 10) return 'Cold';
  if (temp < 20) return 'Cool';
  if (temp < 30) return 'Warm';
  return 'Hot';
});

console.log(fahrenheit());   // 71.6
console.log(description());  // 'Warm'

celsius.set(5);

console.log(fahrenheit());   // 41
console.log(description());  // 'Cold'

The important thing: fahrenheit and description only recompute when celsius changes, not on every change detection cycle.

Effects

Effects run side effects whenever the signals they read change. Use them for things like logging, analytics, syncing to localStorage, etc.

import { effect } from '@angular/core';

const theme = signal<'light' | 'dark'>('dark');

effect(() => {
  // Runs immediately, then again whenever theme() changes
  document.documentElement.setAttribute('data-theme', theme());
  localStorage.setItem('theme', theme());
  console.log(`Theme changed to: ${theme()}`);
});

⚠️ Effects must be created inside an injection context — typically in a component’s constructor or via inject().

Signals in Components

Here is a complete, practical example — a shopping cart using signals:

import { Component, signal, computed } from '@angular/core';
import { CommonModule } from '@angular/common';

interface CartItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

@Component({
  selector: 'app-cart',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="cart">
      <h2>Cart ({{ itemCount() }} items)</h2>
      
      @for (item of items(); track item.id) {
        <div class="cart-item">
          <span>{{ item.name }}</span>
          <span>x{{ item.quantity }}</span>
          <span>{{ item.price * item.quantity | currency }}</span>
          <button (click)="remove(item.id)">Remove</button>
        </div>
      }
      
      <div class="cart-total">
        <strong>Total: {{ total() | currency }}</strong>
      </div>
      
      <button (click)="addSample()">Add Sample Item</button>
    </div>
  `
})
export class CartComponent {
  items = signal<CartItem[]>([
    { id: 1, name: 'Angular Course', price: 49.99, quantity: 1 },
    { id: 2, name: 'RxJS Deep Dive', price: 29.99, quantity: 2 },
  ]);

  // Derived values — these update automatically when items() changes
  itemCount = computed(() => this.items().reduce((sum, i) => sum + i.quantity, 0));
  total = computed(() => this.items().reduce((sum, i) => sum + (i.price * i.quantity), 0));

  remove(id: number) {
    this.items.update(items => items.filter(i => i.id !== id));
  }

  addSample() {
    this.items.update(items => [
      ...items,
      { id: Date.now(), name: 'New Item', price: 9.99, quantity: 1 }
    ]);
  }
}

Signals vs RxJS: When to Use Which

Both are valid tools — they serve different purposes.

Use CasePrefer
Component local stateSignals
Async operations (HTTP, WebSockets)RxJS
Derived/computed stateSignals
Complex event stream transformationsRxJS
Cross-component shared stateSignals (with a service)
Cancellable requestsRxJS

The good news: toSignal() and toObservable() let you bridge between the two worlds seamlessly.

import { toSignal } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';

@Component({ ... })
export class UsersComponent {
  private http = inject(HttpClient);

  // Convert an Observable to a Signal
  users = toSignal(this.http.get<User[]>('/api/users'), { initialValue: [] });
}

Summary

Signals give you a precise, performant reactivity model that scales from simple counters to complex shared application state. The mental model is clear: signals hold state, computed signals derive state, effects react to state.

In Part 3, we’ll build on this foundation and tackle Angular Services, Dependency Injection, and HTTP — the glue that connects your components to the real world.

Continue to → Part 3: Services, DI & HTTP