El Renacimiento de Angular: De Componentes a Señales
Tabla de Contenidos
Abrir tabla de contenidos
- La Era de Zone.js: Magia a un Costo
- El Surgimiento de las Signals: Reactividad de Grano Fino
- Comparativa Técnica: Flujo Tradicional vs. Signals
- ¿Por qué es la tendencia actual? (El camino a Zoneless)
- Implementación en el Código
- La Sinergia: RxJS + Signals
- ¿Cuándo usar qué?
- El concepto Pro:
effect()vstap() - Conclusión
La Era de Zone.js: Magia a un Costo
Históricamente, Angular ha dependido de Zone.js para saber cuándo actualizar la vista. Esto no necesariamente es malo, pero tiene sus desventajas, a lo largo de este post analizaremos a fondo el porqué de este cambio.
¿Cómo funciona el flujo tradicional?
Zone.js crea un “contexto” alrededor de todas las operaciones asíncronas (clicks, HTTP requests, timeouts). Cuando algo sucede, Zone.js le avisa a Angular: “Oye, algo pasó, checa todo”.
- Detección de Cambios Global: Angular inicia un recorrido desde el componente raíz hasta el último hijo (árbol abajo) comparando los valores actuales con los anteriores.
- El Problema: Aunque es efectivo, es ineficiente en aplicaciones grandes. Angular no sabe exactamente qué cambió, solo sabe que algo cambió, por lo que tiene que re-evaluar demasiadas cosas. Esto se conoce como Dirty Checking.
El Surgimiento de las Signals: Reactividad de Grano Fino
Las Signals (introducidas a partir de la v16) cambian el paradigma: en lugar de que Angular “adivine” si hubo cambios, el dato mismo es el que le avisa a Angular.
¿Qué es una Signal?
Imagina una Signal como un “contenedor” de un valor que mantiene una lista de quién lo está usando (suscriptores).
- Productor: La Signal que cambia.
- Consumidor: El componente o la plantilla que lee la Signal.
La Mecánica Interna: El Grafo de Dependencias
A diferencia de los Observables de RxJS, las Signals no requieren que te suscribas manualmente (evitas el | async o los .subscribe()). Angular crea un grafo de dependencias de forma automática. Si una Signal cambia, Angular sabe exactamente qué parte del DOM debe actualizarse sin tener que recorrer todo el árbol de componentes.
Comparativa Técnica: Flujo Tradicional vs. Signals
| Característica | Flujo Tradicional (Zone.js) | Flujo Reactivo (Signals) |
|---|---|---|
| Detección | Basada en eventos asíncronos. | Basada en cambios de estado específicos. |
| Granularidad | De arriba hacia abajo (Árbol completo). | Grano fino (Solo lo afectado). |
| Rendimiento | Consume más CPU en apps complejas. | Optimizado; evita comparaciones innecesarias. |
| DX (Experiencia Dev) | Más “mágica” pero difícil de debuguear. | Más explícita y predecible. |
¿Por qué es la tendencia actual? (El camino a Zoneless)
La meta final de Angular es ser Zoneless. Al usar Signals, podemos eventualmente quitar Zone.js de nuestros polyfills.ts.
Ventajas de esta tendencia:
- Bundle Size más ligero: Al eliminar Zone.js, tu aplicación pesa menos desde el arranque.
- Mejor Core Web Vitals: Al no bloquear el hilo principal con detecciones de cambios globales, la interacción es más fluida (INP - Interaction to Next Paint).
- Interoperabilidad con RxJS: No vienen a reemplazar a RxJS, sino a complementarlo. Usas RxJS para flujos asíncronos complejos (streams) y Signals para el estado de la interfaz.
Implementación en el Código
Implementar una signal es muy sencillo, basta con usar la función signal() de Angular. A continuación te muestro un ejemplo de cómo implementar una signal en Angular.
// Antes (Tradicional)
count = 0;
increment() { this.count++; }
// Ahora (Signals)
count = signal(0);
doubleCount = computed(() => this.count() * 2); // Se actualiza solo si count cambia
increment() {
this.count.update(v => v + 1);
}
La Sinergia: RxJS + Signals
Aunque las Signals manejan la reactividad en la vista de forma asombrosa, RxJS sigue siendo el rey para manejar flujos asíncronos complejos (como peticiones HTTP, WebSockets o debounce de inputs). La clave está en el paquete @angular/core/rxjs-interop.
1. De Observable a Signal: toSignal()
Este es el caso más común: recibes datos de una API (Observable) y quieres mostrarlos en la UI de forma eficiente sin usar el pipe async.
- Beneficio: Te olvidas de las fugas de memoria (memory leaks) porque
toSignalmaneja la suscripción y desuscripción automáticamente según el ciclo de vida del componente. - Comportamiento: Como una Signal siempre debe tener un valor, puedes pasarle un
initialValue.
// En tu servicio o componente
users$ = this.http.get<User[]>('/api/users');
users = toSignal(this.users$, { initialValue: [] });
// En el template ya no usas el pipe async:
// <ul> @for (user of users(); track user.id) { ... } </ul>
2. De Signal a Observable: toObservable()
A veces necesitas reaccionar a un cambio de una Signal para disparar un efecto secundario asíncrono, como una búsqueda en la base de datos cada que un filtro cambia.
- Caso de uso: Tienes una Signal
searchQuery(). Cada que cambie, quieres hacer una petición HTTP, pero quieres usarswitchMappara cancelar peticiones anteriores.
query = signal('');
// Convertimos la signal a observable para usar operadores de RxJS
results$ = toObservable(this.query).pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(q => this.userService.search(q))
);
¿Cuándo usar qué?
Es de suma importancia saber cuando usar cada una de estas herramientas, ya que no son excluyentes, sino complementarias. A continuación te muestro una tabla comparativa para que puedas tomar la mejor decisión.
| Escenario | Herramienta Ideal | Razón |
|---|---|---|
| Estado de la UI | Signals | Síncronas, simples, sin suscripciones manuales y performance de grano fino. |
| Eventos Asíncronos | RxJS | Manejo de tiempo, reintentos (retry), cancelación y composición de streams. |
| Derivación de datos | Signals (computed) | Se ejecutan de forma perezosa (lazy) y solo cuando es necesario. |
| Orquestación de APIs | RxJS | Los operadores como combineLatest o forkJoin son mucho más potentes para esto. |
El concepto Pro: effect() vs tap()
En RxJS usamos tap() para efectos secundarios dentro de un stream. En el mundo de las Signals, tenemos effect().
Un effect() se ejecuta al menos una vez y luego cada vez que cualquier Signal leída dentro de él cambia. Es perfecto para:
- Logs de analítica.
- Guardar datos en
localStorage. - Manipulaciones manuales del DOM (si de plano es necesario).
Ojo aquí: No uses
effect()para cambiar otros estados (Signals), porque podrías crear ciclos infinitos. Para eso existecomputed().
Conclusión
Las Signals no mataron a RxJS; al contrario, lo liberaron de la carga de tener que gestionar la renderización de la UI, permitiéndole brillar en lo que mejor hace: gestionar flujos de datos complejos en el tiempo.
Las Signals no son solo un “feature” nuevo, sino los cimientos sobre los que Angular se está reconstruyendo para competir en rendimiento con frameworks como Solid.js o Qwik, pero manteniendo su robustez empresarial.