Angular 22 no llega con grandes anuncios, pero sí con cambios que van a afectar a cómo escribes componentes, formularios y servicios a partir de ahora. Varias APIs llevan meses en modo experimental: en esta versión pasan a estables. Y hay una decisión por defecto que, si tienes proyectos en producción, necesitas conocer antes de actualizar. Todo ello, explicado con ejemplos de código.

Angular 22 llegó el 3 de junio de 2026 sin grandes titulares. Pero lo que trajo fue algo más interesante para quienes trabajan a diario con el framework de Front-End preferido por las empresas: estabilidad. Varias de las APIs más esperadas de los últimos años pasaron de developer preview a producción, y el framework tomó decisiones que van a afectar directamente a cómo escribes componentes a partir de ahora.
Si llevas tiempo siguiendo la hoja de ruta de Angular, este lanzamiento es el momento en el que muchas de las apuestas del equipo empiezan a tener más sentido. Si tienes algún proyecto en producción con Angular 19 o 20, hay cosas que tienes que saber antes de ejecutar el típico ng update de cada versión. Y si acabas de empezar a aprender Angular, la versión 22 es probablemente el mejor punto de entrada que ha tenido Angular en años.
Vamos por partes...
Qué significa "versión de consolidación" en Angular (y por qué no es nada malo)
En el mundo del software, hablar de una versión de consolidación suena casi a excusa: "No hay novedades, pero estabilizamos cosas." En realidad, en el ciclo de Angular, llegar a este punto es la consecuencia directa de haber apostado fuerte durante varios años por un modelo reactivo basado en signals.
Las Señales o Signals comenzaron a asomar la cabeza en Angular desde su versión 16, ¡de hace 3 años! Poco a poco, desde entonces, cada versión ha ido incorporando APIs relacionadas: formularios basados en señales, recursos asíncronos, detección de cambios sin Zone.js... todo en modo experimental o developer preview. Lo que hace la versión 22 es dar el paso definitivo: declarar esas APIs como estables, lo que significa que puedes usarlas en producción sin miedo a que fallen o a que cambien la interfaz entre versiones.
Eso no es un detallito sin importancia. En Angular, que algo sea estable es un compromiso. Significa que el equipo de desarrollo de Google se compromete a mantener la compatibilidad hacia atrás y a avisar con (mucho) tiempo antes de cualquier cambio rompedor.
Angular sigue un ciclo de versiones (más o menos) semestral. Las versiones principales (como la 22) se publican aproximadamente cada seis meses, y cada API pasa por las fases experimental → developer preview → stable antes de considerarse apta para producción.
Hay cuatro cambios en Angular 22 que merece la pena entender bien: el nuevo comportamiento por defecto de la detección de cambios, los Signal Forms, las APIs de recursos asíncronos, y las mejoras en servicios e inyección de dependencias. Los veremos uno a uno con código.
OnPush por defecto: el cambio que más va a afectar a tu código
Este era uno de esos cambios que la comunidad de Angular llevaba años esperando. Todos los componentes nuevos van a usar ChangeDetectionStrategy.OnPush por defecto, sin que tengas que especificarlo. Y los componentes existentes van a recibir ChangeDetectionStrategy.Eager al ejecutar ng update.
Para entender por qué esto importa tanto, hay que recordar cómo funciona la detección de cambios en Angular. La estrategia histórica por defecto (ahora renombrada a Eager) revisa el árbol de componentes completo cada vez que ocurre cualquier evento: un clic, una respuesta HTTP, un setTimeout... Angular pregunta a todos los componentes si han cambiado, aunque la mayoría no hayan hecho nada.
Con OnPush, Angular solo comprueba un componente cuando:
- Cambia la referencia de alguno de sus
@Input()
- Se emite un evento desde el propio componente
- Se llama explícitamente a
markForCheck()
- Cambia una señal que el template esté leyendo
El resultado es que Angular hace muchos menos ciclos de detección, y eso se traduce directamente en mejor rendimiento. Especialmente en aplicaciones grandes, claro.
Antes de Angular 22, un componente nuevo sin configuración explícita usaba Default:
// Angular < 22: estrategia Default implícita (comprueba siempre)
@Component({
selector: 'app-contador',
template: `<p>{{ valor }}</p>`
})
export class ContadorComponent {
contador = 0;
}
En Angular 22, ese mismo componente usa OnPush automáticamente. Si migras con ng update, tus componentes existentes quedan así:
// Después de ng update: se añade Eager automáticamente
@Component({
selector: 'app-contador',
changeDetection: ChangeDetectionStrategy.Eager, // añadido por el migrador
template: `<p>{{ valor }}</p>`
})
export class ContadorComponent {
contador = 0;
}
Y un componente nuevo que crees ya, desde Angular 22, hereda directamente OnPush y usa señales:
// Angular 22: OnPush implícito, sin necesidad de declararlo
@Component({
selector: 'app-contador',
template: `<p>{{ contador() }}</p>`
})
export class ContadorComponent {
contador = signal(0);
incrementar() {
this.contador.update(v => v + 1);
}
}
Lo importante aquí es que ChangeDetectionStrategy.Default queda "deprecado" (marcado como obsoleto) y tiene prevista su eliminación en Angular 24. No hay prisa inmediata (aunque la próxima versión será a final de año), pero conviene ir actualizando los componentes migrables a OnPush (o simplemente quitar el Eager que añadió el migrador, si el componente ya usa signals).
Dato curioso: Angular ofrece OnPush desde las primeras versiones (Angular 2 en septiembre de 2016). La mayoría de referencias de rendimiento lo recomendaban como "obligatorio" desde el primer día. La diferencia ahora es que no hace falta que lo sepas: cualquier componente nuevo ya funciona así sin que configures nada.
Con señales en el template y OnPush por defecto, Angular sabe exactamente qué ha cambiado y cuándo. Eso es exactamente el modelo zoneless al que lleva apuntando el framework desde hace años, y que ha permitido también la llegada de los formularios basados en señales.
Signal Forms ya son estables: así cambia la forma de trabajar con formularios
Los formularios siempre han sido uno de los puntos más complejos de Angular. Los Reactive Forms resolvieron muchos problemas de los formularios Template-driven, pero trajeron sus propias 💩: FormControl, FormGroup, FormBuilder, tipado débil en muchas versiones, suscripciones a valueChanges... Mucha fontanería para cosas que en principio no deberían ser tan complicadas.
Los Signal Forms abordan el problema desde otro punto de vista: el estado del formulario vive en signals, no en observables. El tipado es fuerte desde el primer momento, y la integración con el sistema de detección de cambios de Angular es directa y eficiente.
Un formulario de login con la API clásica de Reactive Forms tiene esta pinta:
// Reactive Forms clásico
import { FormBuilder, Validators } from '@angular/forms';
@Component({ /* ... */ })
export class LoginComponent {
private fb = inject(FormBuilder);
loginForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', Validators.required]
});
onSubmit() {
if (this.loginForm.valid) {
console.log(this.loginForm.value); // { email: string | null, password: string | null }
}
}
}
El mismo formulario con Signal Forms en Angular 22:
// Signal Forms (estable en Angular 22)
import { form, control, required, email } from '@angular/forms/signals';
@Component({ /* ... */ })
export class LoginComponent {
loginForm = form({
email: control('', { validators: [required, email] }),
password: control('', { validators: [required] })
});
onSubmit() {
if (this.loginForm.valid()) {
const { email, password } = this.loginForm.value(); // tipado completo
// lógica de autenticación
}
}
}
En la plantilla, usas la directiva FormField para vincular cada campo:
<input [formField]="usuarioForm.nombre" id="nombre" />
@if (usuarioForm.nombre().touched() && usuarioForm.nombre().invalid()) {
@if (usuarioForm.nombre().getError('required')) {
<p>El nombre es obligatorio</p>
}
@if (usuarioForm.nombre().getError('minLength'); as err) {
<p>Mínimo {{ err.minLength }} caracteres</p>
}
}
La diferencia más visible: invalid() y valid() son funciones que devuelven un booleano reactivo (leen un signal internamente), así que el binding en el template es directo. No necesitas suscripciones ni una tubería async para seguir el estado del formulario.
Y fíjate en getError(): es una novedad de esta versión que evita tener que iterar sobre el objeto errors para encontrar lo que buscas, con tipado correcto incluido. Ya no necesitas hacer field.errors?.['minLength'].
Otras mejoras en Signal Forms que llegan en esta versión:
- Nuevos validadores
minDate() y maxDate() para campos de fecha.
- Debounce en blur: puedes retrasar la validación hasta que el usuario abandona el campo con
debounce(campo, 'blur').
reloadValidation(): relanza los validadores asíncronos de un campo y sus descendientes. Es el equivalente al updateValueAndValidity() de los formularios clásicos.
markAsTouched() ahora marca el campo y todos sus descendientes, no solo el campo en sí.
- Los componentes basados en
ControlValueAccessor son ya compatibles con Signal Forms sin cambios en cómo los usas.
Los Reactive Forms "clásicos" no desaparecen. Siguen siendo perfectamente válidos y no están marcados como obsoletos. Pero si empiezas un proyecto nuevo o tienes capacidad de migrar formularios, Signal Forms es la dirección a la que apunta el framework y la que deberíamos seguir.
resource() y httpResource : datos asíncronos sin todas las vueltas de antes
La recepción de datos en Angular siempre ha tenido su ritual: inyectas HttpClient, te suscribes con una tubería async o con subscribe() en el componente, gestionas los estados de carga y error manualmente, debes recordar desuscribirte en ngOnDestroy... Es manejable, pero repetitivo.
httpResource y resource() son la respuesta de Angular a eso. Las dos APIs llevan meses en developer preview y en la versión 22 pasan a estables.
resource() es la API base para derivar datos asíncronos a partir de señales. httpResource es una capa específica para HTTP que encapsula HttpClient y expone el resultado como un objeto reactivo con tres señales: value(), isLoading() y error():
import { Component, signal } from '@angular/core';
import { httpResource } from '@angular/common/http';
interface Producto {
id: number;
nombre: string;
precio: number;
}
@Component({
selector: 'app-catalogo',
template: `
@if (productos.isLoading()) {
<p>Cargando catálogo...</p>
} @else if (productos.error()) {
<p>Error al cargar los productos. Inténtalo de nuevo.</p>
} @else {
@for (producto of productos.value(); track producto.id) {
<div class="producto">
<h3>{{ producto.nombre }}</h3>
<span>{{ producto.precio | currency }}</span>
</div>
}
}
<div class="paginacion">
<button (click)="pagina.update(p => p - 1)" [disabled]="pagina() === 1">Anterior</button>
<span>Página {{ pagina() }}</span>
<button (click)="pagina.update(p => p + 1)">Siguiente</button>
</div>
`
})
export class CatalogoComponent {
pagina = signal(1);
// Se recarga automáticamente cada vez que cambia `pagina`
productos = httpResource<Producto[]>(() => ({
url: `/api/productos`,
params: { page: this.pagina(), limit: 20 }
}));
}
Lo más interesante de este patrón: el recurso sabe que depende de pagina() porque lo lee durante su configuración. Cuando el usuario cambia de página, httpResource lanza la nueva petición de manera automática: sin ngOnChanges, sin efectos secundarios adicionales, sin subscribe.
rxResource es la variante para cuando necesitas más control y prefieres trabajar con observables en la capa de datos (por ejemplo, si ya tienes servicios que devuelven Observable). La interfaz pública es la misma, y el template no nota la diferencia.
Por cierto, desde Angular 22, el cliente HTTP usa la Fetch API internamente en lugar de XMLHttpRequest. Este cambio era el recomendado desde hace tiempo para aplicaciones con SSR, y ahora es el comportamiento por defecto para todos. Si ya tenías withFetch() en la configuración, puedes eliminarlo: está "deprecado", y la migración automática se encarga de quitarlo también.
Con estos tres bloques (detección de cambios eficiente, formularios reactivos puros y datos asíncronos declarativos) el código de los componentes Angular es bastante diferente al de hace tres o cuatro versiones. Pero hay dos cambios más en Angular 22 que afectan a cómo organizas y estructuras los servicios.
@Service e injectAsync: menos fontanería, más intención
Hay dos novedades en Angular 22 que tocan la capa de servicios y la de inyección de dependencias.
La primera es @Service(), un decorador nuevo que actúa como alias semántico de @Injectable({ providedIn: 'root' }). Hace lo mismo, pero con menos código y con un nombre que deja más claro de qué va:
// Antes
@Injectable({ providedIn: 'root' })
export class ProductosService {
// ...
}
// Angular 22: mismo resultado, más claro
import { Service } from '@angular/core';
@Service()
export class ProductosService {
// ...
}
Si necesitas registrar el servicio a nivel de componente o ruta en lugar de a nivel de aplicación, puedes desactivar el comportamiento por defecto:
@Service({ autoProvided: false })
export class CarritoService {
// Este servicio se proveerá manualmente donde se necesite
}
La segunda novedad es injectAsync, que permite la carga diferida de servicios. En aplicaciones grandes, hay servicios que solo se necesitan bajo demanda (un editor de texto enriquecido, una biblioteca de gráficos, un módulo de exportación a PDF...). Hasta ahora, cargar esos servicios de manera "perezosa" (lazy loading, o sea, solo cuando se necesiten y no antes) requería soluciones a medida. Ahora con injectAsync es una línea:
import { Component } from '@angular/core';
import { injectAsync } from '@angular/core';
@Component({
selector: 'app-editor',
template: `
<button (click)="abrirEditor()">Abrir editor</button>
`
})
export class EditorComponent {
// El servicio NO se carga hasta que alguien lo llame
private editorService = injectAsync(() => import('./editor-avanzado.service'));
async abrirEditor() {
const service = await this.editorService();
service.inicializar();
}
}
Así, el bundle inicial de la aplicación no incluye EditorAvanzadoService. Solo se descarga cuando el usuario hace clic. El impacto en el tiempo de carga inicial puede ser grande si tienes servicios pesados.
debounced(): controlar el tiempo en señales reactivas
Si alguna vez has implementado un buscador con señales que no sature el servidor con cada pulsación de teclado, sabrás que la solución clásica pasaba por meter RxJS de por medio. Angular 22 añade debounced() (por ahora experimental) para hacer esto de forma nativa:
import { signal } from '@angular/core';
import { debounced } from '@angular/core'; // experimental en v22
@Component({
template: `
<input type="search" [formField]="queryForm" placeholder="Buscar..." />
@if (queryStable.isLoading()) {
<span>Escribiendo...</span>
}
@for (item of resultados.value(); track item.id) {
<li>{{ item.nombre }}</li>
}
`
})
export class BuscadorComponent {
protected readonly query = signal('');
protected readonly queryForm = form(this.query);
// Solo "publica" el valor cuando lleva 300ms sin cambiar
protected readonly queryStable = debounced(() => this.query(), 300);
protected readonly resultados = inject(BuscadorService).buscar(
this.queryStable.value
);
}
debounced() devuelve un Resource, no otro signal. Eso significa que tiene estados: puede estar resolviendo (el usuario sigue escribiendo) o estable. Puedes usar queryStable.isLoading() en la plantilla para mostrar el indicador de "escribiendo..." mientras el valor no se ha asentado.
El segundo parámetro puede ser también una función que devuelve una Promise si necesitas un control más fino de cuándo se estabiliza el valor.
Accesibilidad en Angular: de serie en Angular 22
Angular Aria lleva un tiempo en el framework y en la versión 22 alcanza estado estable. Es un conjunto de primitivas headless (sin interfaz de usuario ni estilos) para construir componentes accesibles: diálogos, menús desplegables, tooltips, elementos con foco controlado.
- Lo que hace Angular Aria por ti: gestiona los atributos ARIA (Accessible Rich Internet Applications) correctos, el manejo del foco (incluido el focus trap en modales) y la navegación por teclado.
- Lo que no hace: dictar el aspecto visual. Tú decides el CSS. Angular Aria se ocupa de que el componente sea usable con un lector de pantalla.
Se usan así:
import { Component } from '@angular/core';
import { CdkDialog, CdkDialogContainer } from '@angular/cdk/dialog'; // parte de Angular Aria
@Component({
selector: 'app-confirmacion',
template: `
<button (click)="abrirDialogo()">Eliminar cuenta</button>
`
})
export class ConfirmacionComponent {
private dialog = inject(CdkDialog);
abrirDialogo() {
this.dialog.open(DialogoConfirmacionComponent, {
// Angular gestiona: aria-modal, focus trap, Esc para cerrar
ariaLabel: 'Confirmar eliminación de cuenta'
});
}
}
La accesibilidad web no es opcional, es una obligación legal en la UE desde junio de 2025. Tener primitivas estables en el propio framework elimina la excusa de "ya lo haremos más tarde".
Cómo migrar a Angular 22: lo que hace ng update por ti (y lo que no)
Lo primero que debes saber es que TypeScript 6 y Node 22 obligatorios. Esta versión sube el mínimo requerido. TypeScript 5.x queda fuera, y Node 20 también. Comprueba tu entorno antes de actualizar.
La migración desde Angular 21 es, como siempre, bastante cómoda.
El proceso estándar es tan solo hacer esto:
ng update @angular/core@22 @angular/cli@22
Lo que hace el migrador automáticamente:
- Añade
changeDetection: ChangeDetectionStrategy.Eager a todos los componentes que no tenían estrategia explícita (preserva el comportamiento anterior)
- Actualiza las referencias a
ChangeDetectionStrategy.Default a ChangeDetectionStrategy.Eager
- Migra las dependencias de paquetes a sus versiones compatibles con Angular 22
Lo que no hace solo:
- Reescribir tus formularios a Signal Forms (eso es una decisión personal o de equipo, no para un migrador automático)
- Cambiar tus
httpClient.get() con subscribe() a httpResource (lo mismo)
- Convertir tus
@Injectable() a @Service() (es algo opcional y cosmético)
El resultado es que, tras ng update, tu aplicación sigue funcionando exactamente igual que antes. Los componentes migrados a Eager se comportan como el antiguo Default. Desde ahí, puedes ir modernizando componente a componente, en tu propio ritmo.
Un consejo práctico: empieza por los componentes más aislados (sin hijos, sin comunicación compleja) y elimina el Eager que añadió el migrador. Si el componente usa señales en el template, OnPush debería funcionar sin cambios adicionales. Si usa mutaciones directas de objetos sin signals, vas a tener que refactorizarlo un poco.
Angular y los agentes de IA
Angular 22 incluye, en modo experimental, un servidor MCP (Model Context Protocol) integrado directamente en el Angular CLI. Si no conoces el protocolo MCP, en pocas palabras: es el estándar que permite que los asistentes de IA (como GitHub Copilot, Codex, Antigravity o cualquier agente compatible) interactúen con herramientas externas de forma estructurada, en lugar de limitarse a generar texto.
Tu agente de IA favorito conectado al MCP de Angular puede arrancar y parar el servidor de desarrollo, leer la estructura del proyecto, entender qué componentes existen, qué dependencias hay y en qué estado está el workspace.
¿Para qué sirve esto en el día a día?:
- Pedirle a un agente que migre un componente de
Eager a OnPush con su patrón de señales correspondiente, ejecute los tests y te cuente si algo ha fallado
- Generar componentes completos que sigan las convenciones del proyecto (rutas, estructura de carpetas, nombres)
- Delegar tareas de refactorización repetitivas mientras tú te ocupas de lo más complejo
El equipo de Angular también ha publicado algunos Skills, y herramientas dentro del MCP como modernize y onpush_zoneless_migration que los agentes pueden usar directamente. La idea es que el asistente de IA no solo escriba código sino que conozca el contexto de tu aplicación y ejecute pasos reales, no solo los sugiera.
Es una apuesta muy grande del equipo de Angular para la actual transición entre "autocompletadores" avanzados a agentes de IA colaboradores que entienden la estructura del proyecto.
Todavía es experimental, pero la dirección que lleva es muy buena.
Te dejo todos los detalles sobre el servidor MCP de Angular.
Angular 22: sin adornos pero muy importante
Puede que Angular 22 no sea la versión más emocionante para hacer titulares, pero sí es probablemente la más útil si llevas proyectos reales con este framework. Las APIs de Señales que llevaban meses en modo preview son ya estables y aptas para producción. OnPush pasa a ser el comportamiento por defecto, lo que obliga a escribir componentes más predecibles y eficientes. Y la integración con agentes de IA empieza a ser útil de verdad.
No hay que hacer nada de golpe. ng update garantiza que tu aplicación existente no se rompe. Lo que sí puedes hacer es ir adoptando los nuevos patrones de forma gradual: un formulario aquí, un httpResource allá, un servicio con injectAsync donde toque. Así es como se moderniza un proyecto Angular sin dramas y poco a poco.