Claude
Skills
Sign in
Back

signals-patterns

Included with Lifetime
$97 forever

Angular Signals - Modern reactive state management patterns with computed signals, effects, and interoperability with RxJS

General

What this skill does


# Angular Signals Patterns

**Version:** Angular 16+  
**Status:** Stable (Developer Preview in 16, Stable in 17+)  
**Purpose:** Modern reactive state management without Zone.js overhead

---

## Core Concept

**Signals** are Angular's modern approach to reactive state management. Unlike observables, signals are synchronous, glitch-free, and optimized for change detection.

**Key Benefits:**
- ✅ Simpler mental model than RxJS
- ✅ Better performance (no Zone.js overhead)
- ✅ Fine-grained reactivity
- ✅ Type-safe
- ✅ Works seamlessly with OnPush change detection

---

## Signal Basics

### Creating Signals

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

// Writable signal
const count = signal(0);               // number signal
const name = signal('John');           // string signal
const user = signal<User | null>(null); // object signal with null

// Read value (call as function)
console.log(count());  // 0
console.log(name());   // "John"

// Write value
count.set(5);          // Set to exact value
name.set('Jane');

// Update value (based on current)
count.update(n => n + 1);  // Increment
```

### Computed Signals

Computed signals automatically recalculate when dependencies change:

```typescript
const count = signal(0);

// Derived value
const double = computed(() => count() * 2);
const isEven = computed(() => count() % 2 === 0);
const message = computed(() => 
  `Count is ${count()} and ${isEven() ? 'even' : 'odd'}`
);

console.log(double());   // 0
console.log(message());  // "Count is 0 and even"

count.set(3);

console.log(double());   // 6
console.log(message());  // "Count is 3 and odd"
```

### Effects

Effects run side effects when signals change:

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

const count = signal(0);

// Effect runs when count changes
effect(() => {
  console.log(`Count changed to: ${count()}`);
  localStorage.setItem('count', count().toString());
});

count.set(5);  // Logs: "Count changed to: 5"
```

---

## Pattern 1: Component State

### Basic Component State

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

@Component({
  selector: 'app-todo-list',
  standalone: true,
  template: `
    <input 
      [value]="newTodo()" 
      (input)="newTodo.set($any($event.target).value)"
    />
    <button (click)="addTodo()">Add</button>
    
    <p>Total: {{ total() }} | Active: {{ active() }} | Completed: {{ completed() }}</p>
    
    @for (todo of todos(); track todo.id) {
      <div>
        <input 
          type="checkbox" 
          [checked]="todo.completed"
          (change)="toggle(todo.id)"
        />
        <span [class.line-through]="todo.completed">
          {{ todo.text }}
        </span>
        <button (click)="remove(todo.id)">×</button>
      </div>
    }
  `
})
export class TodoListComponent {
  // State
  todos = signal<Todo[]>([]);
  newTodo = signal('');
  
  // Computed
  total = computed(() => this.todos().length);
  active = computed(() => this.todos().filter(t => !t.completed).length);
  completed = computed(() => this.todos().filter(t => t.completed).length);
  
  addTodo() {
    if (this.newTodo().trim()) {
      this.todos.update(todos => [
        ...todos,
        {
          id: Date.now().toString(),
          text: this.newTodo(),
          completed: false
        }
      ]);
      this.newTodo.set('');
    }
  }
  
  toggle(id: string) {
    this.todos.update(todos =>
      todos.map(t => t.id === id ? { ...t, completed: !t.completed } : t)
    );
  }
  
  remove(id: string) {
    this.todos.update(todos => todos.filter(t => t.id !== id));
  }
}
```

---

## Pattern 2: Derived State

### Computed from Multiple Signals

```typescript
@Component({...})
export class ShoppingCartComponent {
  items = signal<CartItem[]>([]);
  taxRate = signal(0.08);
  shippingCost = signal(5.99);
  
  // Computed from items
  subtotal = computed(() =>
    this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
  );
  
  // Computed from subtotal and taxRate
  tax = computed(() => this.subtotal() * this.taxRate());
  
  // Computed from multiple signals
  total = computed(() =>
    this.subtotal() + this.tax() + this.shippingCost()
  );
  
  // Computed boolean
  hasItems = computed(() => this.items().length > 0);
  canCheckout = computed(() => 
    this.hasItems() && this.total() > 0
  );
}
```

---

## Pattern 3: Signal Arrays

### Immutable Array Updates

```typescript
@Component({...})
export class ListComponent {
  items = signal<Item[]>([]);
  
  // Add item
  addItem(item: Item) {
    this.items.update(current => [...current, item]);
  }
  
  // Remove item
  removeItem(id: string) {
    this.items.update(current => current.filter(item => item.id !== id));
  }
  
  // Update item
  updateItem(id: string, updates: Partial<Item>) {
    this.items.update(current =>
      current.map(item => 
        item.id === id ? { ...item, ...updates } : item
      )
    );
  }
  
  // Sort items
  sortBy(key: keyof Item) {
    this.items.update(current => 
      [...current].sort((a, b) => a[key] > b[key] ? 1 : -1)
    );
  }
  
  // Filter items
  filteredItems = computed(() =>
    this.items().filter(item => item.active)
  );
}
```

---

## Pattern 4: Signal Objects

### Nested Object Updates

```typescript
interface User {
  id: string;
  name: string;
  email: string;
  preferences: {
    theme: 'light' | 'dark';
    language: string;
  };
}

@Component({...})
export class UserProfileComponent {
  user = signal<User>({
    id: '1',
    name: 'John',
    email: '[email protected]',
    preferences: {
      theme: 'light',
      language: 'en'
    }
  });
  
  // Update top-level property
  updateName(name: string) {
    this.user.update(u => ({ ...u, name }));
  }
  
  // Update nested property
  updateTheme(theme: 'light' | 'dark') {
    this.user.update(u => ({
      ...u,
      preferences: {
        ...u.preferences,
        theme
      }
    }));
  }
  
  // Computed from nested property
  isDarkMode = computed(() => this.user().preferences.theme === 'dark');
}
```

---

## Pattern 5: Loading States

### Common Loading Pattern

```typescript
interface LoadingState<T> {
  loading: boolean;
  data: T | null;
  error: string | null;
}

@Component({...})
export class DataComponent {
  private http = inject(HttpClient);
  
  state = signal<LoadingState<Product[]>>({
    loading: false,
    data: null,
    error: null
  });
  
  // Computed
  isLoading = computed(() => this.state().loading);
  hasError = computed(() => this.state().error !== null);
  hasData = computed(() => this.state().data !== null);
  products = computed(() => this.state().data ?? []);
  
  loadData() {
    this.state.update(s => ({ ...s, loading: true, error: null }));
    
    this.http.get<Product[]>('/api/products').subscribe({
      next: data => this.state.set({ loading: false, data, error: null }),
      error: err => this.state.set({ loading: false, data: null, error: err.message })
    });
  }
}
```

---

## Pattern 6: Form State

### Signal-Based Form

```typescript
@Component({
  selector: 'app-signup-form',
  template: `
    <form (submit)="handleSubmit($event)">
      <input 
        type="email"
        [value]="email()"
        (input)="email.set($any($event.target).value)"
        [class.invalid]="emailError()"
      />
      @if (emailError()) {
        <span class="error">{{ emailError() }}</span>
      }
      
      <input 
        type="password"
        [value]="password()"
        (input)="password.set($any($event.target).value)"
      />
      
      <button [disabled]="!isValid()">Sign Up</button>
    </form>
  `
})
export class SignupFormComponent {
  // Form fields
  email = signal('');
  password = signal('');
  
  // Validation
  emailError = computed(() => {
    const value = this.email();
    if (!value) return 'Email is required';
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
      return 'Invalid email format';
    }
    retur

Related in General