Este contenido solo está disponible en Inglés.
También disponible en Español.
Angular Signals: Reactive State Management & Performance Guide
Angular Signals fundamentally change reactive state management, offering cleaner, more performant programming without RxJS overhead. Discover how these 'smart variables' automatically track and update UI, solving fine-grained reactivity. This guide covers writable, computed, effects, real-world examples like forms and shopping carts, and advanced patterns for robust Angular apps.

Remember the last time you debugged a complex RxJS chain with multiple subscriptions, async pipes everywhere, and that one memory leak you couldn't track down? Or when you had to explain to a junior developer why they need to unsubscribe from observables??
Today, I want to share Angular Signals - a new reactive primitive that fundamentally changes how we handle state in Angular applications. By the end of this article, you'll understand how to leverage signals for cleaner, more performant reactive programming without the traditional RxJS overhead.
What are Angular Signals?
Think of signals as "smart variables" that automatically track when they're read and notify when they change. They're like a GPS tracker for your data - always knowing who's watching and efficiently updating only what needs to change.
Angular Signals solve the fundamental problem of fine-grained reactivity: knowing exactly what changed and updating only the affected parts of your UI, without manual subscription management or change detection concerns.
When Should You Use Angular Signals?
Good use cases:
- Component state management that needs reactive updates
- Computed values derived from other reactive sources
- Form state and validation logic
- Shared state between components without services
- Performance-critical UI updates with minimal change detection
When NOT to use Signals:
- HTTP requests and async operations (stick with Observables)
- Complex event streams requiring operators like debounce, throttle
- Integration with existing RxJS-heavy codebases (use interop carefully)
Signals: Your First Implementation
Let's build a practical example: a product counter with real-time price calculation that demonstrates the core signal concepts.
Step 1: Creating Your First Signal
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-product',
template: `
<div>
<h2>Product: {{ productName() }}</h2>
<p>Quantity: {{ quantity() }}</p>
<button (click)="increment()">Add to Cart</button>
</div>
`
})
export class ProductComponent {
// Creating writable signals
productName = signal('Angular Book');
quantity = signal(0);
increment() {
// Updating signal value
this.quantity.set(this.quantity() + 1);
}
}This code creates two signals - notice how we call them as functions in the template. Signals are functions that return their current value when called.
Step 2: Working with Computed Signals
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-product',
template: `
<div>
<p>Quantity: {{ quantity() }}</p>
<p>Price per item: ${{ pricePerItem() }}</p>
<p>Total: ${{ totalPrice() }}</p>
<button (click)="increment()">Add Item</button>
</div>
`
})
export class ProductComponent {
quantity = signal(1);
pricePerItem = signal(29.99);
// Computed signal automatically updates when dependencies change
totalPrice = computed(() => {
return this.quantity() * this.pricePerItem();
});
increment() {
this.quantity.update(q => q + 1);
}
}Computed signals automatically recalculate when their dependencies change. No subscriptions, no manual updates - it just works.
Step 3: Signal Effects for Side Effects
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-product'
})
export class ProductComponent {
quantity = signal(0);
inventory = signal(10);
constructor() {
// Effect runs whenever signals it reads change
effect(() => {
if (this.quantity() > this.inventory()) {
console.log('Warning: Quantity exceeds inventory!');
this.showInventoryWarning = true;
}
});
}
addToCart() {
if (this.quantity() < this.inventory()) {
this.quantity.update(q => q + 1);
}
}
}Effects automatically track signal dependencies and re-run when those signals change - perfect for logging, analytics, or DOM manipulations.
A More Complex Example: Shopping Cart with Filters
Let's build something more realistic - a shopping cart with real-time filtering and calculations:
import { Component, signal, computed } from '@angular/core';
interface Product {
id: number;
name: string;
price: number;
category: string;
inStock: boolean;
}
@Component({
selector: 'app-shopping-cart',
template: `
<div class="cart">
<input
placeholder="Search products..."
(input)="searchTerm.set($event.target.value)"
/>
<select (change)="selectedCategory.set($event.target.value)">
<option value="all">All Categories</option>
<option *ngFor="let cat of categories()" [value]="cat">
{{ cat }}
</option>
</select>
<div class="products">
<div *ngFor="let product of filteredProducts()">
<h3>{{ product.name }}</h3>
<p>${{ product.price }}</p>
<button
(click)="addToCart(product)"
[disabled]="!product.inStock"
>
Add to Cart
</button>
</div>
</div>
<div class="summary">
<p>Items in cart: {{ cartItems().length }}</p>
<p>Total: ${{ cartTotal() }}</p>
<p>With tax (10%): ${{ totalWithTax() }}</p>
</div>
</div>
`
})
export class ShoppingCartComponent {
// State signals
products = signal<Product[]>([
{ id: 1, name: 'Laptop', price: 999, category: 'Electronics', inStock: true },
{ id: 2, name: 'Mouse', price: 29, category: 'Electronics', inStock: true },
{ id: 3, name: 'Desk', price: 299, category: 'Furniture', inStock: false },
{ id: 4, name: 'Chair', price: 199, category: 'Furniture', inStock: true }
]);
cartItems = signal<Product[]>([]);
searchTerm = signal('');
selectedCategory = signal('all');
// Computed signals for derived state
categories = computed(() => {
const cats = new Set(this.products().map(p => p.category));
return Array.from(cats);
});
filteredProducts = computed(() => {
const term = this.searchTerm().toLowerCase();
const category = this.selectedCategory();
return this.products().filter(product => {
const matchesSearch = product.name.toLowerCase().includes(term);
const matchesCategory = category === 'all' || product.category === category;
return matchesSearch && matchesCategory;
});
});
cartTotal = computed(() => {
return this.cartItems().reduce((sum, item) => sum + item.price, 0);
});
totalWithTax = computed(() => {
return this.cartTotal() * 1.1; // 10% tax
});
addToCart(product: Product) {
this.cartItems.update(items => [...items, product]);
}
}This example shows how signals elegantly handle complex reactive relationships without manual subscription management.
Advanced Pattern: Signal-Based Form Validation
Let's build something even more sophisticated - a reactive form with real-time validation using signals:
import { Component, signal, computed, effect } from '@angular/core';
interface FormErrors {
email?: string;
password?: string;
confirmPassword?: string;
}
@Component({
selector: 'app-signup-form',
template: `
<form (submit)="handleSubmit($event)">
<div>
<input
type="email"
placeholder="Email"
[value]="email()"
(input)="email.set($event.target.value)"
[class.error]="errors().email"
/>
<span class="error-msg">{{ errors().email }}</span>
</div>
<div>
<input
type="password"
placeholder="Password"
[value]="password()"
(input)="password.set($event.target.value)"
[class.error]="errors().password"
/>
<span class="error-msg">{{ errors().password }}</span>
</div>
<div>
<input
type="password"
placeholder="Confirm Password"
[value]="confirmPassword()"
(input)="confirmPassword.set($event.target.value)"
[class.error]="errors().confirmPassword"
/>
<span class="error-msg">{{ errors().confirmPassword }}</span>
</div>
<button [disabled]="!isFormValid()">
Sign Up
</button>
<div class="strength-meter">
Password Strength: {{ passwordStrength() }}
</div>
</form>
`
})
export class SignupFormComponent {
// Form field signals
email = signal('');
password = signal('');
confirmPassword = signal('');
touched = signal<Set<string>>(new Set());
// Validation rules as computed signals
errors = computed<FormErrors>(() => {
const errors: FormErrors = {};
const touchedFields = this.touched();
// Email validation
if (touchedFields.has('email')) {
const emailValue = this.email();
if (!emailValue) {
errors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailValue)) {
errors.email = 'Invalid email format';
}
}
// Password validation
if (touchedFields.has('password')) {
const pwd = this.password();
if (!pwd) {
errors.password = 'Password is required';
} else if (pwd.length < 8) {
errors.password = 'Password must be at least 8 characters';
}
}
// Confirm password validation
if (touchedFields.has('confirmPassword')) {
if (this.password() !== this.confirmPassword()) {
errors.confirmPassword = 'Passwords do not match';
}
}
return errors;
});
passwordStrength = computed(() => {
const pwd = this.password();
if (pwd.length < 6) return 'Weak';
if (pwd.length < 10) return 'Medium';
if (/[A-Z]/.test(pwd) && /[0-9]/.test(pwd) && /[^A-Za-z0-9]/.test(pwd)) {
return 'Strong';
}
return 'Medium';
});
isFormValid = computed(() => {
return this.email() &&
this.password() &&
this.confirmPassword() &&
Object.keys(this.errors()).length === 0;
});
constructor() {
// Auto-save draft to localStorage
effect(() => {
const draft = {
email: this.email(),
timestamp: Date.now()
};
localStorage.setItem('signupDraft', JSON.stringify(draft));
});
}
markAsTouched(field: string) {
this.touched.update(fields => {
fields.add(field);
return new Set(fields);
});
}
handleSubmit(event: Event) {
event.preventDefault();
if (this.isFormValid()) {
console.log('Form submitted:', {
email: this.email(),
password: this.password()
});
}
}
}This demonstrates how signals can replace complex form libraries with simple, reactive validation logic.
Angular Signals with TypeScript
For TypeScript users, here's how to make your signal implementations type-safe:
// types.ts
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
interface AppState {
currentUser: User | null;
isAuthenticated: boolean;
permissions: string[];
}
// signal-store.service.ts
import { Injectable, signal, computed, Signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class SignalStore {
// Writable signals with explicit types
private _currentUser = signal<User | null>(null);
private _isLoading = signal<boolean>(false);
// Readonly computed signals
readonly currentUser: Signal<User | null> = this._currentUser.asReadonly();
readonly isAuthenticated = computed<boolean>(() => !!this._currentUser());
readonly permissions = computed<string[]>(() => {
const user = this._currentUser();
if (!user) return [];
switch(user.role) {
case 'admin': return ['read', 'write', 'delete', 'admin'];
case 'user': return ['read', 'write'];
case 'guest': return ['read'];
}
});
// Type-safe update methods
login(user: User): void {
this._currentUser.set(user);
}
updateUser(updates: Partial<User>): void {
this._currentUser.update(current =>
current ? { ...current, ...updates } : null
);
}
}
// Usage with TypeScript
@Component({
selector: 'app-profile',
template: `
<div *ngIf="store.currentUser() as user">
<h2>{{ user.name }}</h2>
<p>Role: {{ user.role }}</p>
<ul>
<li *ngFor="let perm of store.permissions()">
{{ perm }}
</li>
</ul>
</div>
`
})
export class ProfileComponent {
constructor(public store: SignalStore) {}
}Advanced Patterns and Best Practices
1. Signal Composition Pattern
Create higher-order signals that combine multiple signal sources:
// Compose multiple signals into a single reactive state
function createPaginatedList<T>(items: Signal<T[]>, pageSize: number) {
const currentPage = signal(0);
const totalPages = computed(() =>
Math.ceil(items().length / pageSize)
);
const paginatedItems = computed(() => {
const start = currentPage() * pageSize;
return items().slice(start, start + pageSize);
});
return {
items: paginatedItems,
currentPage: currentPage.asReadonly(),
totalPages,
nextPage: () => currentPage.update(p => Math.min(p + 1, totalPages() - 1)),
prevPage: () => currentPage.update(p => Math.max(p - 1, 0))
};
}2. Signal Memoization Pattern
Optimize expensive computations with memoized signals:
// Memoize expensive operations
function createMemoizedSignal<T, R>(
source: Signal<T>,
compute: (value: T) => R,
equals?: (a: R, b: R) => boolean
) {
let lastInput: T | undefined;
let lastOutput: R | undefined;
return computed(() => {
const current = source();
if (lastInput === current && lastOutput !== undefined) {
return lastOutput;
}
lastInput = current;
lastOutput = compute(current);
return lastOutput;
}, { equal: equals });
}3. Signal Debouncing Pattern
Implement debounced signals for search and input handling:
// Debounced signal for search inputs
function createDebouncedSignal<T>(initialValue: T, delay: number) {
const immediate = signal(initialValue);
const debounced = signal(initialValue);
let timeoutId: any;
const set = (value: T) => {
immediate.set(value);
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
debounced.set(value);
}, delay);
};
return {
immediate: immediate.asReadonly(),
debounced: debounced.asReadonly(),
set
};
}
// Usage
const search = createDebouncedSignal('', 300);
// search.immediate() - instant value
// search.debounced() - debounced value for API calls4. Signal State Machine Pattern
Build robust state machines with signals:
// State machine using signals
function createStateMachine<T extends string>(
initialState: T,
transitions: Record<T, T[]>
) {
const currentState = signal(initialState);
const canTransitionTo = computed(() => {
return transitions[currentState()] || [];
});
const transitionTo = (newState: T) => {
if (canTransitionTo().includes(newState)) {
currentState.set(newState);
return true;
}
return false;
};
return {
state: currentState.asReadonly(),
canTransitionTo,
transitionTo
};
}Common Pitfalls to Avoid
1. Mutating Objects Inside Signals
// ❌ Don't do this - mutating object won't trigger updates
const user = signal({ name: 'John', age: 30 });
user().name = 'Jane'; // This won't trigger change detection!
// ✅ Do this instead - create new object reference
user.update(u => ({ ...u, name: 'Jane' }));
// Or
user.set({ ...user(), name: 'Jane' });2. Creating Signals Inside Computed
// ❌ Problem example - creates new signal on every computation
const badComputed = computed(() => {
const tempSignal = signal(0); // Don't create signals here!
return tempSignal() + otherSignal();
});
// ✅ Solution - create signals outside computed
const tempSignal = signal(0);
const goodComputed = computed(() => {
return tempSignal() + otherSignal();
});3. Forgetting to Call Signal Functions
// ❌ Avoid this pattern - forgetting parentheses
@Component({
template: `<div>{{ count }}</div>` // Won't update!
})
export class BadComponent {
count = signal(0);
}
// ✅ Preferred approach - always call signals as functions
@Component({
template: `<div>{{ count() }}</div>` // Properly reactive
})
export class GoodComponent {
count = signal(0);
}When NOT to Use Signals
Don't reach for signals when:
- Working with HTTP requests - Observables handle async operations better
- Need complex stream operators (debounce, throttle, retry) - RxJS is more powerful
- Integrating with existing Observable-based APIs - unnecessary conversion overhead
// ❌ Overkill for simple scenarios
const httpResult = signal<Data | null>(null);
this.http.get('/api/data').subscribe(data => {
httpResult.set(data); // Unnecessary conversion
});
// ✅ Simple solution is better
data$ = this.http.get('/api/data');
// Use async pipe in templateSignals vs RxJS Observables
Signals are great for:
- Synchronous state management
- Simple computed values
- Performance-critical UI updates
- Reducing boilerplate code
Consider Observables when you need:
- Async operations → HTTP requests, WebSocket streams
- Complex operators → debounceTime, switchMap, retry
- Event streams → fromEvent, interval, timer
Wrapping Up
Angular Signals are a powerful tool that can dramatically simplify state management in your applications. They bring fine-grained reactivity, automatic dependency tracking, and improved performance to your Angular components.
Key takeaways:
- Signals are functions that hold and track reactive values
- Computed signals automatically derive state from other signals
- Effects handle side effects with automatic dependency tracking
- Signals eliminate manual subscription management and memory leaks
The next time you reach for a Subject or BehaviorSubject for component state, remember signals. Your code will be cleaner, more performant, and easier to reason about.
Have you started using signals in your Angular projects? What patterns have you discovered? Share your experiences in the comments!
If this helped you level up your Angular skills, follow for more modern Angular patterns and best practices! 🚀
Resources
- Angular Signals Official Documentation
- TC39 Signals Proposal Specification
- Angular RFC: Sub-RFC 4: Signal-based Components



