> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 05. Angular Signals

> Master Angular's modern reactive primitive for fine-grained reactivity and optimal performance

<Frame>
  <img src="https://mintcdn.com/devweeekends/AEOaWh79Ur7CdHHv/images/courses/angular-crash-course/signals.svg?fit=max&auto=format&n=AEOaWh79Ur7CdHHv&q=85&s=fb6877f0f486aef442c6c2196145794d" alt="Angular Signals" width="900" height="500" data-path="images/courses/angular-crash-course/signals.svg" />
</Frame>

## Module Overview

<Info>
  **Estimated Time**: 3-4 hours | **Difficulty**: Intermediate | **Prerequisites**: Module 4
</Info>

Signals are Angular's new reactive primitive introduced in v16. They provide fine-grained reactivity, enabling Angular to know exactly which parts of the template need updating, leading to better performance.

**What You'll Learn:**

* Creating and reading signals
* Computed signals for derived state
* Effects for side effects
* Signal patterns and best practices
* Comparison with RxJS observables
* Real-world signal architectures

***

## What are Signals?

```
┌─────────────────────────────────────────────────────────────────────────┐
│                    Signals: Reactive Primitives                          │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   Signal:     Wrapper around value that notifies when value changes     │
│   Computed:   Signal derived from other signals, cached automatically   │
│   Effect:     Side effect that runs when signals change                 │
│                                                                          │
│   ┌───────────────┐                                                     │
│   │  signal(10)   │──────────────┐                                      │
│   └───────────────┘              ▼                                      │
│                           ┌────────────────┐                            │
│   ┌───────────────┐       │                │                            │
│   │  signal(20)   │──────▶│   computed()   │──────▶ Template            │
│   └───────────────┘       │   (cached)     │                            │
│                           └────────────────┘                            │
│                                  │                                       │
│                                  ▼                                       │
│                           ┌────────────────┐                            │
│                           │    effect()    │ ─────▶ Side effects        │
│                           │ (logging, API) │                            │
│                           └────────────────┘                            │
│                                                                          │
│   Key Benefits:                                                          │
│   ✓ Fine-grained reactivity (knows what changed)                        │
│   ✓ Synchronous updates (predictable timing)                            │
│   ✓ No subscription management needed                                   │
│   ✓ Automatic dependency tracking                                       │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘
```

***

## Creating Signals

### WritableSignal

```typescript theme={null}
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <div class="counter">
      <button (click)="decrement()">-</button>
      <span>{{ count() }}</span>
      <button (click)="increment()">+</button>
    </div>
    <button (click)="reset()">Reset</button>
  `
})
export class CounterComponent {
  // Create a writable signal with initial value
  count = signal(0);
  
  increment() {
    // set(): Replace the value
    this.count.set(this.count() + 1);
    
    // OR use update(): Transform based on current value
    // this.count.update(value => value + 1);
  }
  
  decrement() {
    this.count.update(value => Math.max(0, value - 1));
  }
  
  reset() {
    this.count.set(0);
  }
}
```

### Signal Methods

```typescript theme={null}
// Create a signal
const name = signal('Alice');

// Read the value (call the signal)
console.log(name());  // 'Alice'

// Set a new value
name.set('Bob');

// Update based on current value
name.update(current => current.toUpperCase());

// Create read-only version (for public APIs)
const readonlyName = name.asReadonly();
// readonlyName.set('X');  // ERROR: No set method
```

### Signals with Objects/Arrays

```typescript theme={null}
interface User {
  name: string;
  age: number;
  address: {
    city: string;
    country: string;
  };
}

@Component({...})
export class UserComponent {
  user = signal<User>({
    name: 'Alice',
    age: 25,
    address: { city: 'NYC', country: 'USA' }
  });
  
  updateName(name: string) {
    // Must replace entire object (immutable updates)
    this.user.update(user => ({
      ...user,
      name
    }));
  }
  
  updateCity(city: string) {
    this.user.update(user => ({
      ...user,
      address: { ...user.address, city }
    }));
  }
}
```

<Warning>
  **Signals use reference equality by default.** Mutating an object/array won't trigger updates. Always create new references.
</Warning>

***

## Computed Signals

Computed signals derive values from other signals:

```typescript theme={null}
import { Component, signal, computed } from '@angular/core';

@Component({
  selector: 'app-shopping-cart',
  standalone: true,
  template: `
    <h2>Shopping Cart</h2>
    
    @for (item of items(); track item.id) {
      <div class="item">
        <span>{{ item.name }} - ${{ item.price }} x {{ item.quantity }}</span>
        <button (click)="removeItem(item.id)">Remove</button>
      </div>
    }
    
    <div class="summary">
      <p>Items: {{ itemCount() }}</p>
      <p>Subtotal: ${{ subtotal() }}</p>
      <p>Tax (10%): ${{ tax().toFixed(2) }}</p>
      <p><strong>Total: ${{ total().toFixed(2) }}</strong></p>
    </div>
  `
})
export class ShoppingCartComponent {
  items = signal<CartItem[]>([
    { id: 1, name: 'Widget', price: 25, quantity: 2 },
    { id: 2, name: 'Gadget', price: 50, quantity: 1 }
  ]);
  
  // Computed values - automatically cached and updated
  itemCount = computed(() => 
    this.items().reduce((sum, item) => sum + item.quantity, 0)
  );
  
  subtotal = computed(() =>
    this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
  );
  
  tax = computed(() => this.subtotal() * 0.1);
  
  total = computed(() => this.subtotal() + this.tax());
  
  removeItem(id: number) {
    this.items.update(items => items.filter(item => item.id !== id));
    // All computed values automatically recalculate!
  }
}
```

### Computed Signal Properties

```typescript theme={null}
// Computed signals are:
// 1. LAZY: Only computed when read
// 2. CACHED: Recalculated only when dependencies change
// 3. READ-ONLY: Cannot be set directly

const firstName = signal('John');
const lastName = signal('Doe');

const fullName = computed(() => {
  console.log('Computing fullName...');  // Only logs when deps change
  return `${firstName()} ${lastName()}`;
});

// First read: computes and caches
console.log(fullName());  // "John Doe"

// Subsequent reads: uses cache
console.log(fullName());  // "John Doe" (no recomputation)

// Change a dependency
firstName.set('Jane');

// Next read: recomputes
console.log(fullName());  // "Jane Doe"
```

***

## Effects

Effects run side effects when signals change:

```typescript theme={null}
import { Component, signal, effect, inject, OnInit } from '@angular/core';

@Component({
  selector: 'app-settings',
  standalone: true,
  template: `
    <select (change)="setTheme($event)">
      <option value="light">Light</option>
      <option value="dark">Dark</option>
      <option value="system">System</option>
    </select>
    
    <input 
      type="text" 
      [value]="searchQuery()"
      (input)="searchQuery.set($event.target.value)"
      placeholder="Search..."
    />
  `
})
export class SettingsComponent {
  private http = inject(HttpClient);
  
  theme = signal<'light' | 'dark' | 'system'>('light');
  searchQuery = signal('');
  
  constructor() {
    // Effect: Sync theme to document
    effect(() => {
      document.documentElement.setAttribute('data-theme', this.theme());
      localStorage.setItem('theme', this.theme());
    });
    
    // Effect: Debounced search
    effect((onCleanup) => {
      const query = this.searchQuery();
      if (!query) return;
      
      const timeout = setTimeout(() => {
        this.performSearch(query);
      }, 300);
      
      // Cleanup function for previous effect run
      onCleanup(() => clearTimeout(timeout));
    });
  }
  
  setTheme(event: Event) {
    const select = event.target as HTMLSelectElement;
    this.theme.set(select.value as any);
  }
  
  private performSearch(query: string) {
    console.log('Searching for:', query);
  }
}
```

### Effect Options

```typescript theme={null}
// Disable automatic tracking for specific reads
effect(() => {
  // This signal is NOT tracked (won't trigger re-run)
  const config = untracked(() => this.config());
  
  // This signal IS tracked
  const user = this.user();
  
  console.log(`User: ${user.name}, Config: ${config.apiUrl}`);
});

// Allow signal writes inside effect (use sparingly!)
effect(() => {
  const value = this.sourceSignal();
  // Normally writing signals in effects is forbidden
  this.derivedSignal.set(transform(value));
}, { allowSignalWrites: true });
```

<Warning>
  **Avoid writing signals in effects** when possible. Use computed signals for derived state. Effects with `allowSignalWrites` can lead to infinite loops if not careful.
</Warning>

***

## Signal Inputs and Outputs

Modern Angular provides signal-based component communication:

### Signal Inputs

```typescript theme={null}
import { Component, input, computed } from '@angular/core';

@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `
    <div class="user-card">
      <h3>{{ name() }}</h3>
      @if (email()) {
        <p>{{ email() }}</p>
      }
      <span class="initials">{{ initials() }}</span>
    </div>
  `
})
export class UserCardComponent {
  // Required input
  name = input.required<string>();
  
  // Optional input with default
  email = input<string>('');
  
  // Transform input value
  age = input(0, { transform: (value: string | number) => Number(value) });
  
  // Alias for template attribute
  userId = input.required<number>({ alias: 'id' });
  
  // Computed from inputs
  initials = computed(() => {
    const parts = this.name().split(' ');
    return parts.map(p => p[0]).join('').toUpperCase();
  });
}
```

```html theme={null}
<!-- Using the component -->
<app-user-card 
  [name]="userName" 
  [email]="userEmail" 
  [id]="user.id"
/>
```

### Model Inputs (Two-Way Binding)

```typescript theme={null}
import { Component, model } from '@angular/core';

@Component({
  selector: 'app-rating',
  standalone: true,
  template: `
    <div class="rating">
      @for (star of stars; track star) {
        <button 
          (click)="value.set(star)"
          [class.active]="star <= value()"
        >
          ★
        </button>
      }
    </div>
  `
})
export class RatingComponent {
  // Two-way bindable signal
  value = model(0);
  
  stars = [1, 2, 3, 4, 5];
}
```

```html theme={null}
<!-- Two-way binding syntax -->
<app-rating [(value)]="userRating" />
```

### Signal Outputs

```typescript theme={null}
import { Component, output, OutputEmitterRef } from '@angular/core';

@Component({
  selector: 'app-search',
  standalone: true,
  template: `
    <input 
      #searchInput
      (keyup.enter)="search.emit(searchInput.value)"
    />
    <button (click)="search.emit(searchInput.value)">Search</button>
    <button (click)="clear.emit()">Clear</button>
  `
})
export class SearchComponent {
  // Output without value
  clear = output();
  
  // Output with value
  search = output<string>();
  
  // Aliased output
  submitted = output<string>({ alias: 'onSearch' });
}
```

***

## Signal Patterns

### Form State Management

```typescript theme={null}
@Component({
  selector: 'app-signup-form',
  standalone: true,
  template: `
    <form (submit)="onSubmit($event)">
      <input 
        [value]="formData().email"
        (input)="updateField('email', $event)"
        placeholder="Email"
      />
      @if (errors().email) {
        <span class="error">{{ errors().email }}</span>
      }
      
      <input 
        type="password"
        [value]="formData().password"
        (input)="updateField('password', $event)"
        placeholder="Password"
      />
      @if (errors().password) {
        <span class="error">{{ errors().password }}</span>
      }
      
      <button [disabled]="!isValid() || isSubmitting()">
        {{ isSubmitting() ? 'Submitting...' : 'Sign Up' }}
      </button>
    </form>
  `
})
export class SignupFormComponent {
  formData = signal({
    email: '',
    password: ''
  });
  
  errors = computed(() => {
    const data = this.formData();
    return {
      email: this.validateEmail(data.email),
      password: this.validatePassword(data.password)
    };
  });
  
  isValid = computed(() => {
    const errs = this.errors();
    return !errs.email && !errs.password;
  });
  
  isSubmitting = signal(false);
  
  updateField(field: 'email' | 'password', event: Event) {
    const value = (event.target as HTMLInputElement).value;
    this.formData.update(data => ({ ...data, [field]: value }));
  }
  
  private validateEmail(email: string): string | null {
    if (!email) return 'Email is required';
    if (!email.includes('@')) return 'Invalid email format';
    return null;
  }
  
  private validatePassword(password: string): string | null {
    if (!password) return 'Password is required';
    if (password.length < 8) return 'Password must be 8+ characters';
    return null;
  }
  
  async onSubmit(event: Event) {
    event.preventDefault();
    if (!this.isValid()) return;
    
    this.isSubmitting.set(true);
    try {
      await this.authService.signup(this.formData());
    } finally {
      this.isSubmitting.set(false);
    }
  }
}
```

### Async Data Loading

```typescript theme={null}
@Component({
  selector: 'app-user-profile',
  standalone: true,
  template: `
    @if (loading()) {
      <div class="skeleton">Loading...</div>
    } @else if (error()) {
      <div class="error">
        {{ error() }}
        <button (click)="loadUser()">Retry</button>
      </div>
    } @else if (user(); as user) {
      <div class="profile">
        <h1>{{ user.name }}</h1>
        <p>{{ user.email }}</p>
      </div>
    }
  `
})
export class UserProfileComponent implements OnInit {
  private userService = inject(UserService);
  private route = inject(ActivatedRoute);
  
  user = signal<User | null>(null);
  loading = signal(false);
  error = signal<string | null>(null);
  
  ngOnInit() {
    this.loadUser();
  }
  
  async loadUser() {
    this.loading.set(true);
    this.error.set(null);
    
    try {
      const userId = this.route.snapshot.params['id'];
      const user = await firstValueFrom(
        this.userService.getUser(userId)
      );
      this.user.set(user);
    } catch (err) {
      this.error.set('Failed to load user');
    } finally {
      this.loading.set(false);
    }
  }
}
```

### Linked Signals

```typescript theme={null}
import { signal, linkedSignal } from '@angular/core';

@Component({...})
export class ProductComponent {
  products = signal([
    { id: 1, name: 'Widget', variants: ['small', 'large'] },
    { id: 2, name: 'Gadget', variants: ['blue', 'red', 'green'] }
  ]);
  
  selectedProduct = signal(this.products()[0]);
  
  // Linked signal: resets when selectedProduct changes
  selectedVariant = linkedSignal(() => this.selectedProduct().variants[0]);
  
  selectProduct(product: Product) {
    this.selectedProduct.set(product);
    // selectedVariant automatically resets to first variant!
  }
}
```

***

## Signal vs RxJS

```
┌─────────────────────────────────────────────────────────────────────────┐
│                    When to Use Signals vs RxJS                           │
├───────────────────────────────┬─────────────────────────────────────────┤
│         Use Signals           │           Use RxJS                       │
├───────────────────────────────┼─────────────────────────────────────────┤
│ • Synchronous state           │ • Async operations (HTTP)               │
│ • Component-local state       │ • Complex event streams                 │
│ • Derived/computed values     │ • Debouncing, throttling                │
│ • Template bindings           │ • Retries, polling                      │
│ • Simple parent-child comms   │ • Race conditions, cancellation         │
│ • Form state management       │ • WebSocket streams                     │
└───────────────────────────────┴─────────────────────────────────────────┘
```

### Converting Between Signals and RxJS

```typescript theme={null}
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { Observable, interval } from 'rxjs';

@Component({...})
export class ConversionComponent {
  private http = inject(HttpClient);
  
  // Observable → Signal (for templates)
  users$ = this.http.get<User[]>('/api/users');
  users = toSignal(this.users$, { initialValue: [] });
  
  // Signal → Observable (for RxJS operators)
  searchQuery = signal('');
  searchResults$ = toObservable(this.searchQuery).pipe(
    debounceTime(300),
    distinctUntilChanged(),
    switchMap(query => this.searchService.search(query))
  );
  
  // Convert back to signal for template
  searchResults = toSignal(this.searchResults$, { initialValue: [] });
}
```

***

## Practice Exercise

<Card title="Exercise: Build a Todo App with Signals" icon="dumbbell">
  Create a todo app with:

  1. Add/remove/toggle todos
  2. Filter (all, active, completed)
  3. Computed stats (total, completed, remaining)
  4. Persist to localStorage via effect
</Card>

<Accordion title="Solution">
  ```typescript theme={null}
  interface Todo {
    id: number;
    text: string;
    completed: boolean;
  }

  @Component({
    selector: 'app-todos',
    standalone: true,
    template: `
      <div class="todo-app">
        <input 
          #newTodo
          (keyup.enter)="addTodo(newTodo.value); newTodo.value = ''"
          placeholder="Add todo..."
        />
        
        <div class="filters">
          <button 
            (click)="filter.set('all')"
            [class.active]="filter() === 'all'"
          >All ({{ stats().total }})</button>
          <button 
            (click)="filter.set('active')"
            [class.active]="filter() === 'active'"
          >Active ({{ stats().remaining }})</button>
          <button 
            (click)="filter.set('completed')"
            [class.active]="filter() === 'completed'"
          >Completed ({{ stats().completed }})</button>
        </div>
        
        <ul class="todo-list">
          @for (todo of filteredTodos(); track todo.id) {
            <li [class.completed]="todo.completed">
              <input 
                type="checkbox" 
                [checked]="todo.completed"
                (change)="toggleTodo(todo.id)"
              />
              <span>{{ todo.text }}</span>
              <button (click)="removeTodo(todo.id)">×</button>
            </li>
          }
        </ul>
        
        @if (stats().completed > 0) {
          <button (click)="clearCompleted()">
            Clear completed
          </button>
        }
      </div>
    `
  })
  export class TodosComponent {
    private nextId = 1;
    
    todos = signal<Todo[]>(this.loadFromStorage());
    filter = signal<'all' | 'active' | 'completed'>('all');
    
    filteredTodos = computed(() => {
      const todos = this.todos();
      const filter = this.filter();
      
      switch (filter) {
        case 'active': return todos.filter(t => !t.completed);
        case 'completed': return todos.filter(t => t.completed);
        default: return todos;
      }
    });
    
    stats = computed(() => {
      const todos = this.todos();
      const completed = todos.filter(t => t.completed).length;
      return {
        total: todos.length,
        completed,
        remaining: todos.length - completed
      };
    });
    
    constructor() {
      // Persist on changes
      effect(() => {
        localStorage.setItem('todos', JSON.stringify(this.todos()));
      });
    }
    
    addTodo(text: string) {
      if (!text.trim()) return;
      this.todos.update(todos => [
        ...todos,
        { id: this.nextId++, text, completed: false }
      ]);
    }
    
    toggleTodo(id: number) {
      this.todos.update(todos =>
        todos.map(t => t.id === id ? { ...t, completed: !t.completed } : t)
      );
    }
    
    removeTodo(id: number) {
      this.todos.update(todos => todos.filter(t => t.id !== id));
    }
    
    clearCompleted() {
      this.todos.update(todos => todos.filter(t => !t.completed));
    }
    
    private loadFromStorage(): Todo[] {
      const data = localStorage.getItem('todos');
      const todos = data ? JSON.parse(data) : [];
      this.nextId = Math.max(...todos.map(t => t.id), 0) + 1;
      return todos;
    }
  }
  ```
</Accordion>

***

## Summary

<Steps>
  <Step title="signal()">
    Create reactive containers for values that notify on change
  </Step>

  <Step title="computed()">
    Derive values automatically with caching and lazy evaluation
  </Step>

  <Step title="effect()">
    Run side effects when signals change, with cleanup support
  </Step>

  <Step title="Signal Inputs/Outputs">
    Modern component communication with input(), model(), output()
  </Step>

  <Step title="RxJS Interop">
    Convert between signals and observables with toSignal/toObservable
  </Step>
</Steps>

***

## Next Steps

<Card title="Next: Routing & Navigation" icon="arrow-right" href="/courses/angular-crash-course/06-routing">
  Build multi-page applications with Angular Router
</Card>
