Angular 17 marcó un antes y un después con la introducción estable de Signals — un nuevo primitivo reactivo que simplifica drásticamente la gestión del estado en componentes. Si llevas tiempo con Angular y estás acostumbrado a RxJS, este artículo es para ti.
¿Qué problema resuelven los Signals?
Antes de Signals, cualquier cambio de estado en un componente requería que Angular recorriera todo el árbol de componentes buscando qué había cambiado (markForCheck, ChangeDetectionStrategy.OnPush, etc.). Con Signals, Angular sabe exactamente qué partes del DOM dependen de qué datos — sin recorrer nada.
// Antes — con BehaviorSubject
private count$ = new BehaviorSubject(0);
count = this.count$.asObservable();
increment() { this.count$.next(this.count$.value + 1); }
// Ahora — con Signals
count = signal(0);
increment() { this.count.update(v => v + 1); }
La diferencia es visual, pero el impacto en rendimiento es real.
Los tres primitivos básicos
signal() — Estado mutable
import { signal } from '@angular/core';
@Component({...})
export class CartComponent {
items = signal<CartItem[]>([]);
addItem(item: CartItem) {
this.items.update(current => [...current, item]);
}
removeItem(id: string) {
this.items.update(items => items.filter(i => i.id !== id));
}
}
computed() — Estado derivado
import { signal, computed } from '@angular/core';
total = computed(() =>
this.items().reduce((sum, item) => sum + item.price, 0)
);
hasItems = computed(() => this.items().length > 0);
computed() solo se recalcula cuando sus dependencias cambian. Angular detecta las dependencias automáticamente en tiempo de ejecución.
effect() — Efectos secundarios
import { effect } from '@angular/core';
constructor() {
effect(() => {
// Se ejecuta cuando `items` cambia
localStorage.setItem('cart', JSON.stringify(this.items()));
});
}
Úsalo con cuidado: los efectos son para sincronización con sistemas externos (localStorage, analytics, etc.), no para transformar estado.
Cuándo NO usar Signals
Signals no reemplaza RxJS en todos los casos. Sigue usando Observables para:
- Operaciones asíncronas que necesitan operadores como
switchMap,debounceTime,retry - Streams de eventos (WebSockets, formularios reactivos)
- Lógica compleja con
forkJoin,combineLatest
Para conectar Signals con Observables tienes toSignal():
import { toSignal } from '@angular/core/rxjs-interop';
products = toSignal(this.productService.getAll(), {
initialValue: []
});
Signals en producción: lo que hemos aprendido
En proyectos recientes migrando componentes de Angular 15 a 17+, hemos observado:
- Reducción del 40-60% en ciclos de detección de cambios en páginas con listas dinámicas
- Código más legible: el flujo de datos es explícito sin necesidad de
asyncpipe en cada template - Menos bugs sutiles relacionados con subscripciones no canceladas (el
takeUntilDestroyedsigue siendo necesario con Observables, pero cada vez lo usamos menos)
Migración gradual
No tienes que migrar todo de golpe. Angular está diseñado para que coexistan:
// Un mismo componente puede mezclar ambos enfoques
@Component({...})
export class SearchComponent {
// Signal para estado local sencillo
searchTerm = signal('');
// Observable para la petición HTTP con lógica de debounce
results$ = toObservable(this.searchTerm).pipe(
debounceTime(300),
switchMap(term => this.api.search(term))
);
// Signal derivado del Observable para el template
results = toSignal(this.results$, { initialValue: [] });
}
Conclusión
Signals no es una moda pasajera — es la dirección que el equipo de Angular ha elegido para los próximos años. Para estado local de componentes y datos derivados, ya es nuestra opción por defecto en sigalo. RxJS sigue siendo imprescindible para lógica de negocio compleja.
Si tienes un proyecto en Angular 15+ y quieres empezar la migración, la superficie de cambio es pequeña y el beneficio es inmediato.
¿Tienes un proyecto Angular y quieres revisarlo? Escríbenos — estaremos encantados de hacer una auditoría.