Angular Signals provide a new way to handle reactive state with fine-grained reactivity and improved performance.
What Are Signals?
Signals are wrappers around values that notify interested consumers when the value changes.
Basic Signals
typescript
import { signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<h1>{{ count() }}</h1>
<button (click)="increment()">Increment</button>
`
})
export class CounterComponent {
count = signal(0);
increment() {
this.count.update(value => value + 1);
}
}Signal Methods
typescript
// Create signal
const count = signal(0);
// Read value
console.log(count()); // 0
// Set value
count.set(5);
// Update based on previous value
count.update(value => value + 1);
// Mutate (for objects)
const user = signal({ name: 'John' });
user.mutate(value => value.name = 'Jane');Computed Signals
Derive values from other signals:
typescript
@Component({
selector: 'app-cart',
template: `
<p>Items: {{ items().length }}</p>
<p>Total: ${{ total() }}</p>
`
})
export class CartComponent {
items = signal<CartItem[]>([]);
total = computed(() =>
this.items().reduce((sum, item) => sum + item.price, 0)
);
tax = computed(() => this.total() * 0.1);
grandTotal = computed(() => this.total() + this.tax());
}Effects
Run side effects when signals change:
typescript
@Component({ ... })
export class UserComponent implements OnDestroy {
user = signal<User | null>(null);
private destroyRef = inject(DestroyRef);
constructor() {
effect(() => {
const user = this.user();
if (user) {
console.log('User changed:', user.name);
localStorage.setItem('user', JSON.stringify(user));
}
}, { allowSignalWrites: true });
}
}Signal Inputs
typescript
@Component({
selector: 'app-user-card',
template: `
<h2>{{ name() }}</h2>
<p>{{ email() }}</p>
`
})
export class UserCardComponent {
name = input<string>('');
email = input<string>('');
user = computed(() => ({
name: this.name(),
email: this.email()
}));
}Required Inputs
typescript
export class UserCardComponent {
name = input.required<string>();
email = input.required<string>();
}Signal Models (Two-Way Binding)
typescript
@Component({
selector: 'app-input',
template: `
<input [(ngModel)]="value()" />
`
})
export class InputComponent {
value = model<string>('');
}Signal Outputs
typescript
@Component({
selector: 'app-button',
template: `
<button (click)="handleClick()">Click</button>
`
})
export class ButtonComponent {
clicked = output<void>();
handleClick() {
this.clicked.emit();
}
}Signals in Services
typescript
@Injectable({ providedIn: 'root' })
export class UserService {
private users = signal<User[]>([]);
private filter = signal<string>('');
usersFiltered = computed(() =>
this.users().filter(user =>
user.name.includes(this.filter())
)
);
loadUsers() {
this.users.set([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
]);
}
setFilter(filter: string) {
this.filter.set(filter);
}
}RxJS Interop
typescript
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
@Component({ ... })
export class DataComponent {
data = signal<Data[]>([]);
// Signal to Observable
data$ = toObservable(this.data);
// Observable to Signal
users = toSignal(
this.http.get<User[]>('/api/users'),
{ initialValue: [] }
);
}Best Practices
- 1Use signals for local state - Component and service state
- 2Use computed for derived state - Calculated values
- 3Use effects for side effects - Logging, persistence
- 4Keep effects simple - Avoid complex logic
- 5Use input/output for components - Component communication
Angular Signals provide a modern, reactive approach to state management with excellent performance and developer experience.