- name
- angular-signals
- user-invocable
- false
- description
- Use when building Angular 16+ applications requiring fine-grained reactive state management and zone-less change detection.
- allowed-tools
- ["Bash","Read"]
# Angular Signals
Master Angular Signals for building reactive applications with
fine-grained reactivity and improved performance.
## Signal Basics
### Creating and Using Signals
```typescript
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<div>
<p>Count: {{ count() }}</p>
<p>Double: {{ doubleCount() }}</p>
<button (click)="increment()">+</button>
<button (click)="decrement()">-</button>
<button (click)="reset()">Reset</button>
</div>
`
})
export class CounterComponent {
// Writable signal
count = signal(0);
// Computed signal
doubleCount = computed(() => this.count() * 2);
constructor() {
// Effect runs when count changes
effect(() => {
console.log(`Count is: ${this.count()}`);
});
}
increment() {
this.count.update(value => value + 1);
}
decrement() {
this.count.update(value => value - 1);
}
reset() {
this.count.set(0);
}
}
```
### Signal Methods
```typescript
import { signal } from '@angular/core';
// Create signal
const count = signal(0);
// set - replace value
count.set(5);
// update - transform current value
count.update(value => value + 1);
// mutate - modify object (experimental)
const user = signal({ name: 'John', age: 30 });
user.mutate(value => {
value.age = 31; // Mutate in place
});
// Read value
const current = count(); // Call as function
```
## Computed Signals
### Basic Computed
```typescript
import { signal, computed } from '@angular/core';
const firstName = signal('John');
const lastName = signal('Doe');
// Computed signal
const fullName = computed(() => {
return `${firstName()} ${lastName()}`;
});
console.log(fullName()); // John Doe
firstName.set('Jane');
console.log(fullName()); // Jane Doe (automatically updates)
```
### Complex Computed
```typescript
interface Product {
id: number;
name: string;
price: number;
quantity: number;
}
@Component({
selector: 'app-cart'
})
export class CartComponent {
items = signal<Product[]>([]);
// Computed: total items
itemCount = computed(() => {
return this.items().reduce((sum, item) => sum + item.quantity, 0);
});
// Computed: subtotal
subtotal = computed(() => {
return this.items().reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
});
// Computed: tax
tax = computed(() => this.subtotal() * 0.08);
// Computed: total
total = computed(() => this.subtotal() + this.tax());
// Computed: formatted total
formattedTotal = computed(() => {
return `$${this.total().toFixed(2)}`;
});
}
```
### Chained Computed
```typescript
const count = signal(1);
const doubled = computed(() => count() * 2);
const quadrupled = computed(() => doubled() * 2);
const formatted = computed(() => `Count: ${quadrupled()}`);
console.log(formatted()); // Count: 4
count.set(2);
console.log(formatted()); // Count: 8
```
## Effects
### Basic Effects
```typescript
import { Component, signal, effect } from '@angular/core';
@Component({
selector: 'app-logger'
})
export class LoggerComponent {
count = signal(0);
constructor() {
// Effect runs when count changes
effect(() => {
console.log(`Count changed to: ${this.count()}`);
});
}
increment() {
this.count.update(v => v + 1); // Triggers effect
}
}
```
### Effect Cleanup
```typescript
import { effect } from '@angular/core';
const count = signal(0);
effect((onCleanup) => {
const timer = setInterval(() => {
console.log(count());
}, 1000);
// Cleanup function
onCleanup(() => {
clearInterval(timer);
});
});
```
### Conditional Effects
```typescript
import { effect, signal } from '@angular/core';
const enabled = signal(true);
const count = signal(0);
effect(() => {
// Only run if enabled
if (!enabled()) return;
console.log(`Count: ${count()}`);
});
```
## Signal Inputs
### Component Inputs as Signals
```typescript
import { Component, input, computed } from '@angular/core';
@Component({
selector: 'app-user-profile',
standalone: true,
template: `
<div>
<h2>{{ displayName() }}</h2>
<p>Age: {{ age() }}</p>
<p>Is adult: {{ isAdult() }}</p>
</div>
`
})
export class UserProfileComponent {
// Signal inputs (Angular 17.1+)
firstName = input.required<string>();
lastName = input.required<string>();
age = input(0); // Optional with default
// Computed from inputs
displayName = computed(() =>
`${this.firstName()} ${this.lastName()}`
);
isAdult = computed(() => this.age() >= 18);
}
// Usage
<app-user-profile
[firstName]="'John'"
[lastName]="'Doe'"
[age]="30"
/>
```
### Transform Input Signals
```typescript
import { Component, input } from '@angular/core';
@Component({
selector: 'app-formatted-text'
})
export class FormattedTextComponent {
// Transform input
text = input('', {
transform: (value: string) => value.toUpperCase()
});
// Alias input
label = input('', { alias: 'labelText' });
}
// Usage
<app-formatted-text
[text]="'hello'"
[labelText]="'Name'"
/>
```
## Signal Outputs
### Component Outputs as Signals
```typescript
import { Component, output } from '@angular/core';
@Component({
selector: 'app-button',
template: `
<button (click)="handleClick()">
{{ label() }}
</button>
`
})
export class ButtonComponent {
label = input('Click me');
// Signal output (Angular 17.1+)
clicked = output<void>();
valueChanged = output<number>();
private clickCount = signal(0);
handleClick() {
this.clickCount.update(v => v + 1);
this.clicked.emit();
this.valueChanged.emit(this.clickCount());
}
}
// Usage
<app-button
(clicked)="onClicked()"
(valueChanged)="onValueChanged($event)"
/>
```
## Signal Queries
### ViewChild with Signals
```typescript
import { Component, viewChild, ElementRef, afterNextRender } from '@angular/core';
@Component({
selector: 'app-input-focus',
template: `
<input #inputElement type="text" />
<button (click)="focusInput()">Focus</button>
`
})
export class InputFocusComponent {
// Signal-based viewChild
inputElement = viewChild<ElementRef>('inputElement');
constructor() {
afterNextRender(() => {
// Access element after render
const element = this.inputElement()?.nativeElement;
if (element) {
element.focus();
}
});
}
focusInput() {
this.inputElement()?.nativeElement.focus();
}
}
```
### ViewChildren with Signals
```typescript
import { Component, viewChildren, ElementRef } from '@angular/core';
@Component({
selector: 'app-list',
standalone: true,
template: `
@for (item of items(); track item) {
<div #item>{{ item }}</div>
}
<p>Item count: {{ itemElements().length }}</p>
`
})
export class ListComponent {
items = signal(['A', 'B', 'C']);
// Signal-based viewChildren
itemElements = viewChildren<ElementRef>('item');
logItemCount() {
console.log(`Count: ${this.itemElements().length}`);
}
}
```
### ContentChild with Signals
```typescript
import { Component, contentChild, Directive } from '@angular/core';
@Directive({
selector: '[appHeader]'
})
export class HeaderDirective {}
@Component({
selector: 'app-card',
standalone: true,
template: `
<div class="card">
<ng-content select="[appHeader]" />
<ng-content />
@if (hasHeader()) {
<p>Has custom header</p>
}
</div>
`
})
export class CardComponent {
// Signal-based contentChild
header = contentChild(HeaderDirective);
hasHeader = computed(() => !!this.header());
}
// Usage
<app-card>
<h2 appHeader>Title</h2>
<p>Content</p>
</app-card>
```
## Signals vs Observables
### When to Use Signals
```typescript
// Use signals for synchronous state
@Component({
selector: 'app-counter'
})
export class CounterComponent {
count = signal(0); // Signal for synchronous state
increment() {
this.count.update(v => v + 1);
}
}
```
### When to Use Observables
```typescript
// Use observables for async operations
@Component({
selector: 'app-user-list'
})
export class UserListComponent {
private http = inject(HttpClient);
View on GitHub