Durante años era posible reconocer una aplicación Angular por varias decisiones predeterminadas: NgModules, Zone.js, inputs mediante decoradores, streams RxJS para casi cualquier estado y Reactive Forms para los formularios complejos. Esas piezas siguen existiendo, pero ya no describen por sí solas la dirección del framework.
Angular 22 consolida un modelo distinto: zoneless por defecto, Signals como primitiva reactiva integrada, Signal Forms estable, recursos asíncronos y estrategias de renderizado por ruta. La consecuencia no es que todo el código anterior sea incorrecto. Es que ahora hay más de una respuesta válida y el equipo necesita asignar cada mecanismo al problema que realmente resuelve.
Modernizar no consiste en reemplazar todos los Observables por Signals. Consiste en hacer explícitos el propietario del estado, su temporalidad, los efectos, la frontera asíncrona y el mecanismo que notifica un cambio.
Signals representa valores; RxJS representa secuencias
Un Signal siempre permite leer un valor actual. Angular registra qué computaciones y vistas lo consumen y puede invalidarlas cuando cambia. Esto encaja bien con estado síncrono de interfaz y valores derivados:
readonly query = signal('');
readonly users = signal<readonly User[]>([]);
readonly visibleUsers = computed(() => {
const term = this.query().trim().toLocaleLowerCase();
return this.users().filter(user => user.name.toLocaleLowerCase().includes(term));
});
El valor derivado no necesita una segunda fuente de verdad ni un effect que
copie datos a otro Signal.
RxJS modela secuencias a través del tiempo y ofrece operadores para combinar,
cancelar, limitar, reintentar o coordinar eventos. Un WebSocket, una secuencia
de búsqueda con cancelación, un pipeline con buffering o control de frecuencia,
o una integración existente basada en Observables no deja de ser válido porque
exista computed.
La pregunta útil no es cuál reemplaza al otro, sino qué semántica tiene el dato:
- Valor actual síncrono: Signal.
- Derivación síncrona:
computed. - Valor derivado que también puede ajustarse localmente:
linkedSignal. - Secuencia temporal, cancelación o composición de eventos: RxJS.
- Lectura asíncrona ligada a parámetros reactivos:
resource,httpResourceorxResource, según la fuente. - Sincronización con una API imperativa:
effect, como último puente y no como mecanismo general de propagación.

Esta clasificación evita dos extremos: continuar usando un BehaviorSubject
para cada booleano local o reconstruir con Signals pipelines temporales que
RxJS expresa mejor.
BehaviorSubject no es un antipatrón por sí mismo. El problema aparece cuando
se utiliza únicamente como contenedor mutable para un valor local síncrono que
no necesita composición temporal ni un contrato Observable. En ese caso, un
Signal suele expresar la intención con menos infraestructura. Si el valor forma
parte de un stream público o se combina mediante operadores, cambiarlo sólo por
uniformidad puede empeorar el contrato.
effect no es un sustituto de la arquitectura de estado
effect ejecuta una función cuando cambian los Signals que leyó. Es apropiado
para sincronizar con algo que no participa en el grafo reactivo: logging,
almacenamiento local no autoritativo, una API visual imperativa o una
integración de terceros.
Usarlo para copiar un Signal en otro suele introducir dos fuentes de verdad, orden de ejecución y estados intermedios:
// Evitar: copia reactiva de estado derivado.
effect(() => this.fullName.set(`${this.name()} ${this.surname()}`));
La relación se expresa mejor como dato derivado:
readonly fullName = computed(() => `${this.name()} ${this.surname()}`);
Angular recomienda llegar a effect después de computed y linkedSignal.
No es una regla estética. Es una forma de conservar un grafo donde el origen de
cada valor puede explicarse sin razonar sobre una cadena de escrituras
imperativas.
Zoneless cambia el mecanismo de notificación
Zone.js parcheaba APIs asíncronas del navegador para que Angular supiera cuándo podría haber cambiado el estado. Era un mecanismo general: una tarea asíncrona podía provocar sincronización aunque no hubiera modificado nada visible.
En modo zoneless, Angular depende de notificaciones explícitas: actualizar un
Signal leído por una plantilla, ejecutar un listener, usar AsyncPipe, llamar a
markForCheck, establecer un input mediante las APIs del framework o adjuntar
una vista marcada.
AsyncPipe sigue siendo una integración válida: se suscribe al Observable y
llama internamente a markForCheck, por lo que notifica correctamente en modo
zoneless. No es obligatorio convertir cada Observable con toSignal. Esa
conversión resulta útil cuando el componente necesita leer un valor actual de
forma síncrona o derivarlo junto a otros Signals; AsyncPipe conserva mejor una
frontera que sólo existe para consumo en plantilla. La elección debe responder
al contrato, no a una jerarquía entre APIs modernas y legacy.

La mejora arquitectónica no es simplemente retirar una dependencia. El cambio obliga a que los componentes comuniquen por qué una vista está sucia. También elimina parches globales que dificultaban ciertas trazas e interoperabilidad.
Pero una aplicación actualizada de versión no es necesariamente zoneless-safe. Hay que buscar dependencias en:
NgZone.onStable,onMicrotaskEmptyoisStable;- mutaciones de objetos que la plantilla no observa mediante una notificación;
- callbacks de librerías que actualizan estado fuera de APIs conocidas;
- tests que asumían el comportamiento de
zone.js/testing; - componentes dinámicos y librerías host que alojan código no compatible.
OnPush ayuda a descubrir estas dependencias, pero no es un requisito absoluto
de zoneless. Un componente puede utilizar otra estrategia si notifica los
cambios correctamente. Convertir todas las declaraciones a OnPush sin
observar el comportamiento puede ocultar fallos hasta una ruta infrecuente.
La migración necesita observabilidad, no fe
Una secuencia defendible para una aplicación existente sería:
- Actualizar Angular por versiones soportadas, sin mezclar el cambio con una reescritura funcional.
- Estabilizar tests y medir rutas críticas antes de cambiar detección.
- Introducir
OnPushy Signals en componentes acotados donde el propietario del estado esté claro. - Sustituir derivaciones manuales por
computedy revisar efectos. - Ejecutar una variante zoneless y corregir dependencias observadas.
- Retirar Zone.js sólo cuando build, tests y comportamiento real lo prueben.
Las métricas deberían responder a una hipótesis: trabajo de change detection, tiempo de interacción, tamaño transferido, startup o complejidad eliminada. No debería declararse una mejora de rendimiento sólo porque el bundle ya no contenga Zone.js.
Interoperabilidad: convertir también cambia semántica
toSignal(observable) crea una suscripción inmediatamente. Puede recibir un
valor inicial explícito mediante initialValue, exigir con
requireSync: true que la fuente emita durante la suscripción o devolver
undefined hasta la primera emisión. requireSync elimina ese undefined del
tipo, pero produce un error en runtime si la garantía de sincronía era falsa.
Si el Observable falla, el error se lanza cuando se lee el Signal.
Por defecto, Angular vincula la limpieza de la suscripción al DestroyRef del
contexto que crea el Signal. Eso evita presentar toSignal como una fuga de
memoria automática, pero no hace inocuas las conversiones repetidas: invocarlo
varias veces sobre la misma fuente crea varias suscripciones y puede duplicar
peticiones o efectos antes de que llegue la destrucción del contexto.
toObservable(signal) utiliza un efecto y emite después de que el Signal se
estabilice. Varias escrituras síncronas pueden producir sólo el último valor.
No es un espejo temporal idéntico a un Subject.
Estos detalles importan al migrar código que depende de sincronía, errores o número de suscripciones. El adapter reduce fricción, pero no convierte dos modelos reactivos en equivalentes semánticos.
Una estrategia prudente mantiene la frontera cerca del consumidor:
- un servicio puede conservar Observables si su contrato representa streams;
- un componente puede convertir una vez a Signal para consumo en plantilla;
- una API nueva puede exponer Signals si realmente posee estado síncrono;
- una librería compartida no debería cambiar su contrato público sólo para seguir la moda de una capa consumidora.
resource modela lecturas, no comandos
resource relaciona parámetros reactivos con un loader asíncrono y expone
valor, estado de carga y error mediante Signals. httpResource aplica el mismo
patrón sobre HttpClient; rxResource acepta una fuente Observable.
rxResource no consume únicamente la primera emisión. La primera entrega
resuelve la carga inicial y las posteriores actualizan el valor actual mientras
la suscripción asociada a esos parámetros siga activa. Aun así, conserva la
semántica de un recurso: un valor actual ligado a una petición y a parámetros
reactivos. No preserva el historial del stream ni sustituye operadores de
coordinación, ventanas, buffering, control de frecuencia, reconexión o
combinación. Para un WebSocket, polling complejo o flujo persistente donde la
secuencia sea el contrato, mantener RxJS directamente suele ser más explícito.
Resulta atractivo para lecturas cuya identidad depende de Signals:
readonly userId = input.required<string>();
readonly user = httpResource(() => `/api/users/${this.userId()}`);
No debería usarse sin más para POST, PUT o acciones de negocio. Cuando los
parámetros cambian, un recurso puede cancelar una carga en curso mediante
AbortSignal. Esa semántica es apropiada para una lectura sustituible y puede
ser peligrosa para una mutación que ya ha producido efectos parciales.
Tampoco convierte una respuesta JSON en una instancia fiable del tipo
TypeScript. La documentación de httpResource recomienda validar respuestas
cuando el contrato lo requiera. El tipo estático describe lo que el código
espera; la validación runtime comprueba lo que recibió.
En SSR aparece además una frontera de confidencialidad. Si se serializa el resultado de un recurso dentro del HTML para reutilizarlo durante hidratación, no debe introducirse información específica de un usuario en una página que pueda almacenarse o compartirse.
Signal Forms amplía opciones; no invalida Reactive Forms
Signal Forms construye un FieldTree sobre un modelo mantenido en un writable
Signal. Ofrece acceso tipado, esquema de validación y estado de campo reactivo.
Desde Angular 22 su API se considera estable.
Para aplicaciones nuevas basadas en Signals puede reducir duplicación entre modelo de datos y árbol de controles. Pero una migración debe considerar:
- controles personalizados existentes y su interoperabilidad;
- compatibilidad de las librerías de componentes UI de terceros y si sus
controles funcionan mediante
FormField, el puente deControlValueAccessoro exigen unFormControlclásico; - validadores síncronos y asíncronos;
- formularios generados dinámicamente;
- librerías que consumen
AbstractControl; - tests y utilidades ya construidos;
- estructuras de modelo compatibles: la capa estructural espera objetos y arrays JavaScript simples.
La propia documentación mantiene Reactive Forms como opción estable y adecuada para aplicaciones existentes y escenarios complejos. Una organización no obtiene retorno por reescribir formularios correctos únicamente para uniformar la sintaxis.
Una adopción gradual puede empezar en un formulario nuevo, medir legibilidad y ecosistema, y usar las APIs de compatibilidad cuando el coste esté justificado. La unidad de migración debería ser un flujo verificable, no un porcentaje arbitrario del repositorio.
Angular moderno también cambia dónde se renderiza
Angular permite elegir CSR, SSR, SSG o una estrategia híbrida por ruta. La
hidratación incremental puede mantener partes de la página deshidratadas hasta
un trigger de interacción, viewport o idle, usando límites @defer.
No toda aplicación necesita servidor. Un backoffice autenticado sin requisito SEO puede ser más simple como CSR. Contenido estable puede prerenderizarse. SSR puede aportar HTML inicial para contenido dinámico, pero introduce coste de servidor, serialización, caché, consistencia server/browser y restricciones sobre manipulación directa del DOM.
La estrategia moderna no es activar SSR. Es decidir por ruta dónde debe producirse el HTML y cuándo merece pagarse la hidratación.
Qué arquitectura emerge
Una aplicación Angular actual puede organizarse alrededor de estas fronteras:
- Signals para estado local y derivado;
- RxJS para secuencias y coordinación temporal;
- recursos para lecturas reactivas cancelables;
- comandos explícitos para mutaciones;
- Signal Forms o Reactive Forms según el vertical;
- detección zoneless basada en notificaciones conocidas;
- renderizado elegido según cada ruta.

No es necesario adoptar todas las piezas para considerar moderna una aplicación. La modernidad útil consiste en eliminar mecanismos accidentales y elegir contratos que un equipo pueda explicar, observar y evolucionar.
Conclusión
Signals y zoneless representan una evolución profunda de Angular porque hacen más explícita la relación entre estado y vista. Esa explicitud puede mejorar rendimiento, depuración y mantenibilidad. No borra el valor de RxJS, Reactive Forms ni una aplicación estable.
La migración correcta no se mide por cuántos decoradores, Observables o NgModules desaparecen. Se mide por si disminuyen las fuentes de verdad, las notificaciones implícitas y el trabajo innecesario sin perder comportamiento.
Angular moderno ofrece mejores primitivas. Convertirlas en una arquitectura mejor sigue siendo responsabilidad del equipo.



