Angular 21 has officially shifted the framework to a Signals-first architecture. If you’re building modern web applications, understanding these reactivity primitives is no longer optional—it’s essential.
In this guide, we break down the 10 core pillars of Angular Signals with practical code examples to help you master the most performant version of Angular yet.
What Are Angular Signals?
At its core, a Signal is a wrapper around a value that notifies interested consumers when that value changes. Unlike RxJS, which is built for asynchronous streams, Signals are designed for synchronous state management.
Why the Shift to Signals in Angular 21?
- Zoneless by Default: Angular 21 no longer requires
zone.jsfor new projects. This reduces bundle size and improves performance by only updating the specific part of the UI that changed. - Fine-Grained Updates: Instead of checking the entire component tree, Angular now knows exactly which DOM element needs a refresh.
- Better DX: No more manual subscriptions or
takeUntil(this.destroy$)logic.
Key New Feature: Signal-Based Forms
The most anticipated update in Angular 21 is the Signal Forms API. For years, developers have struggled with the complexity of RxJS-based Reactive Forms. Signal Forms simplify everything.
How to Create a Signal Form
Instead of FormGroup, you now use the form() function. It provides built-in type safety and reactive state without the boilerplate.
import { signal } from '@angular/core';
import { form, field, Validators } from '@angular/forms/signals';
// 1. Define your reactive model
const loginModel = signal({
email: '',
password: ''
});
// 2. Initialize the form
const loginForm = form(loginModel);
// 3. Access values reactively
console.log(loginForm.email().value()); // Returns the current email signal
Benefits of Signal Forms:
- Type Safety: Automatic deep type inference.
- Validation Signals: Each field has signals for
valid(),touched(), anddirty(). - Performance: Updates are lightning-fast because they don’t trigger global change detection.
Mastering the Signals API
1. Basic Signal Creation
Signals are fine-grained reactivity primitives that store a value and notify consumers when that value changes. Use the signal() function to create a reactive primitive.
import { signal } from '@angular/core';
// Create a signal with initial value
const count = signal(0);
const name = signal('Angular');
const isActive = signal(true);
const items = signal<string[]>([]);
// Access signal value
console.log(count()); // 0
console.log(name()); // 'Angular'
2. Reading and Updating Signals
Read signal values by calling them as functions: signal(). Update signals using .set() to replace the entire value or .update(fn) for computed updates. The .update() method receives the current value and returns the new value.
import { signal } from '@angular/core';
export class CounterComponent {
count = signal(0);
// Read signal value
getCount(): number {
return this.count(); // Must call as function
}
// Update with set()
increment(): void {
this.count.set(this.count() + 1);
}
// Update with update()
decrement(): void {
this.count.update(val => val - 1);
}
// Reset to initial value
reset(): void {
this.count.set(0);
}
}
3. Computed Signals
Computed signals are derived signals that automatically update when their dependencies change. Use computed() to create a signal that depends on other signals. Computations are lazy—they only execute when the computed signal is read. Perfect for calculations, filtering, or data transformations.
import { signal, computed } from '@angular/core';
export class CartComponent {
items = signal([
{ name: 'Item 1', price: 10 },
{ name: 'Item 2', price: 20 }
]);
// Computed signal depends on items signal
totalPrice = computed(() => {
return this.items().reduce((sum, item) => sum + item.price, 0);
});
itemCount = computed(() => this.items().length);
addItem(item: { name: string; price: number }): void {
this.items.update(items => [...items, item]);
// totalPrice automatically updates!
}
}
4. Effect Signals
Effects run side logic in response to signal changes. Use effect() to create an effect that automatically reruns whenever any signals it accesses change. Effects are useful for logging, API calls, DOM updates, or any side effects. Avoid circular dependencies.
import { signal, effect } from '@angular/core';
export class NotificationComponent {
message = signal('');
notificationCount = signal(0);
constructor() {
// Effect runs whenever message signal changes
effect(() => {
const msg = this.message();
console.log('Message changed to:', msg);
this.notificationCount.update(n => n + 1);
});
effect(() => {
console.log('Total notifications:', this.notificationCount());
});
}
showMessage(text: string): void {
this.message.set(text);
}
}
5. Signal Input
Replace @Input() decorators with the input() function for better type safety. Use input.required<T>() for required inputs or input<T>(defaultValue) for optional inputs. Input signals can be used in computed signals and effects, making parent-child communication more reactive.
import { Component, input, computed } from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true
})
export class UserCardComponent {
// Input signal with required property
userId = input.required<number>();
// Optional input signal with default value
userName = input<string>('Unknown');
isAdmin = input<boolean>(false);
// Derived computed value from inputs
displayName = computed(() => {
const name = this.userName();
const admin = this.isAdmin();
return admin ? `${name} (Admin)` : name;
});
}
6. Signal Output
Replace @Output() decorators with the output() function. Use .emit() to send values to parent components. This is more type-safe and integrates better with signals than event emitters.
import { Component, output, signal } from '@angular/core';
@Component({
selector: 'app-button-counter',
standalone: true
})
export class ButtonCounterComponent {
count = signal(0);
// Output signal
countChanged = output<number>();
increment(): void {
this.count.update(val => val + 1);
this.countChanged.emit(this.count());
}
reset(): void {
this.count.set(0);
this.countChanged.emit(0);
}
}
7. toSignal() – Observable to Signal
The toSignal() function converts an Observable to a Signal. This is useful when integrating with RxJS-based services or libraries. The signal updates whenever the Observable emits a value.
import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
import { toSignal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-user-list',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserListComponent {
private http = inject(HttpClient);
// Convert Observable to Signal
users = toSignal(
this.http.get<any[]>('/api/users'),
{ initialValue: [] }
);
}
8. Linked Signals
Linked signals create a two-way synchronization between a source signal and a derived signal. Use linkedSignal() to create a signal that automatically syncs when the source changes, but can also be independently updated.
import { Component, signal, linkedSignal, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-price-calculator',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PriceCalculatorComponent {
basePrice = signal(100);
// Linked signal - syncs with basePrice
discountedPrice = linkedSignal(() => {
return Math.round(this.basePrice() * 0.9); // 10% discount
});
}
9. Type Checking Signals
Always define explicit types for signals using angle brackets <T> to ensure type safety. This prevents runtime errors and provides excellent IDE autocomplete.
import { signal, computed, Signal } from '@angular/core';
interface User {
id: number;
name: string;
}
export class UserManagementComponent {
user: Signal<User> = signal({ id: 1, name: 'John Doe' });
users: Signal<User[]> = signal([]);
addUser(user: User): void {
this.users.update(users => [...users, user]);
}
}
10. Signal Utility Functions: isSignal & isWritableSignal
Angular provides utility functions to check if a value is a signal at runtime. Use isSignal() for any signal and isWritableSignal() to check if you can call .set() or .update() on it.
import { signal, isSignal, isWritableSignal, computed } from '@angular/core';
export class SignalValidationComponent {
writableCount = signal(0);
computedValue = computed(() => this.writableCount() * 2);
validate(): void {
console.log(isSignal(this.writableCount)); // true
console.log(isWritableSignal(this.computedValue)); // false
}
}

