Me senté un fin de semana a probar Angular 22 en serio, no solo a leer el changelog. Y la verdad es que se siente distinto a las últimas releases. No es un update más de versión: varias cosas que yo venía mirando de reojo justamente porque decían "experimental" — signals, zoneless, los formularios basados en signals — esta vez las pude usar sin esa etiqueta encima. Y para mí eso cambia bastante cómo encaro un proyecto nuevo.
Angular 22 salió el 3 de junio de 2026. Acá te cuento lo que más me llamó la atención después de meter mano, con ejemplos de código de lo que probé.
OnPush ahora es el default (y tiene todo el sentido)
Este es el cambio que más me gustó, aunque al principio te haga ruido. Desde siempre, cada componente nuevo en Angular usaba la estrategia de change detection Default, que revisa todo el árbol ante cualquier cosa. Ahora el default es OnPush.
En un mundo donde escribís con signals, esto encaja perfecto: el componente solo se re-renderiza cuando un signal o un input que usa realmente cambia, no cuando algo se mueve en cualquier otra parte de la app.
Si necesitás el comportamiento viejo, lo pedís explícito. Lo que antes era Default ahora se llama Eager (y queda deprecado):
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'app-legacy',
changeDetection: ChangeDetectionStrategy.Eager, // reemplaza al viejo 'Default'
template: `...`,
})
export class LegacyCmp {}Lo bueno: si actualizás un proyecto existente, ng update te pone Eager en los componentes que no tenían OnPush seteado, así no te rompe nada. Pero para código nuevo, el default moderno ya viene puesto.
httpResource: fetch reactivo sin subscribe
Esta fue la que me hizo decir "ah, por fin". El Resource API (resource, rxResource y httpResource) dejó de estar en experimental.
httpResource recibe una función que devuelve un request HTTP. Y la clave: esa función es reactiva. Si un signal que usás adentro cambia, el request se vuelve a disparar solo. Sin subscribe, sin async pipe, sin manejar el unsubscribe.
import { httpResource } from '@angular/common/http';
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
@Component({
selector: 'app-flight-search',
changeDetection: ChangeDetectionStrategy.OnPush,
// ...
})
export class FlightSearch {
protected readonly filter = signal({ from: 'Hamburg', to: 'Graz' });
protected readonly flightsResource = httpResource<Flight[]>(
() => ({
url: 'https://api.example.com/flights',
params: {
from: this.filter().from,
to: this.filter().to,
},
}),
{ defaultValue: [] },
);
protected readonly flights = this.flightsResource.value;
protected readonly isLoading = this.flightsResource.isLoading;
protected readonly error = this.flightsResource.error;
}En el template lo consumís directo, sin pipes raros:
@if (flightsResource.isLoading()) {
<div>Cargando...</div>
}
@if (flightsResource.error()) {
<div>Error: {{ flightsResource.error() }}</div>
} @else {
@for (flight of flightsResource.value(); track flight.id) {
<app-flight-card [item]="flight" />
}
}Lo que terminó de convencerme: si caen varios requests seguidos (típico cuando el usuario tipea rápido en un filtro), se queda con el último y cancela los anteriores, sin que vos hagas nada. El que viene de RxJS lo conoce como el comportamiento de switchMap; acá ya viene resuelto. Y el defaultValue te evita lidiar con undefined al arrancar.
Signal Forms: formularios sin el boilerplate de siempre
Si alguna vez sufriste con FormGroup y FormControl, esta sección te va a gustar. Los Signal Forms ya se pueden usar de verdad.
El corazón es la función form(): le pasás un signal con los datos y un esquema de validación. Cada propiedad del formulario termina siendo un signal con su estado (value, dirty, invalid, errors).
import { linkedSignal, inject } from '@angular/core';
import { form, minLength, required } from '@angular/forms/signals';
@Component({ /* ... */ })
export class FlightEdit {
private readonly store = inject(FlightDetailStore);
protected readonly flight = linkedSignal(() =>
normalizeFlight(this.store.flight()),
);
protected readonly flightForm = form(this.flight, (path) => {
required(path.from);
required(path.to);
minLength(path.from, 3);
});
}Y en el template lo enganchás con la directiva formField. Mucho más directo que antes:
<input [formField]="flightForm.from" id="flight-from" />
<div>{{ flightForm.from().errors() | json }}</div>La primera vez que armé un formulario así me di cuenta de que no extrañaba nada del modelo viejo: se acabaron los FormGroup anidados de tres pisos.
El decorator @Service
Un detalle chico pero elegante. Cuántas veces escribiste @Injectable({ providedIn: 'root' }). Ahora hay un atajo que dice lo que realmente querés:
import { Service } from '@angular/core';
// Provisto en root por defecto
@Service()
export class FlightClient {}
// Si NO querés que se auto-provea
@Service({ autoProvided: false })
export class TabRegistry {}Es de esas cosas que no cambian tu vida, pero que cada vez que las usás pensás "qué bien que esto ahora se lea así".
injectAsync y debounced: los detalles que se sienten bien
Dos primitivas nuevas que me parecieron muy prácticas.
injectAsync te deja inyectar dependencias de forma perezosa — solo cuando de verdad se necesitan. Ideal para servicios que cargan librerías pesadas y solo se usan ante una acción puntual del usuario:
import { injectAsync } from '@angular/core';
@Component({ /* ... */ })
export class ReportComponent {
private readonly getReportService = injectAsync(() =>
import('./report.service').then((m) => m.ReportService),
);
async generateReport(): Promise<void> {
const service = await this.getReportService();
service.generate();
}
}El bundle de ReportService recién se carga la primera vez que llamás generateReport(). Combinado con rutas lazy, te da un control finísimo de qué se carga y cuándo. Y si querés adelantarte, tiene la opción prefetch: onIdle para precargar cuando el navegador está libre.
debounced es chiquito pero lo voy a usar mucho. Envuelve un signal y retrasa sus notificaciones — justo lo que querés para un input de búsqueda, sin volver a meter debounceTime de RxJS:
import { debounced, signal } from '@angular/core';
const searchTerm = signal('');
const debouncedTerm = debounced(searchTerm, 300); // 300ms
// Usás debouncedTerm en un httpResource y listo:
// no dispara un request en cada tecla.Zoneless de verdad (adiós Zone.js)
Para la mayoría de las apps ya no necesitás Zone.js. El change detection es explícito y eficiente, disparado por signals, eventos o llamadas manuales. Eso baja el tamaño del bundle, hace el debugging más sano y acerca Angular al rendimiento de JavaScript nativo.
Lo activás al bootstrappear:
import { bootstrapApplication } from '@angular/platform-browser';
import { provideZonelessChangeDetection } from '@angular/core';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
provideZonelessChangeDetection(),
],
}).catch((err) => console.error(err));Ojo con un detalle: en zoneless, los cambios que pasan fuera de Angular (un setTimeout, eventos del navegador) no disparan change detection solos a menos que los envuelvas en APIs de Angular — pero si ya venís trabajando con signals, eso es justo lo que querés.
Lo demás que vale mencionar
No todo merece su propia sección, pero hay varias cosas que suman:
- Vitest es el runner por defecto en proyectos nuevos, en lugar de Karma. Los tests arrancan más rápido y se siente más moderno.
- Hydration incremental por defecto:
provideClientHydration()ya la activa sola (hidrata los componentes a medida que se vuelven visibles, mejorando el Time to Interactive). Si no la querés, la sacás conwithNoIncrementalHydration(). - Componentes selectorless (en preview): podés importar un componente y usarlo en el template sin string selector, evitando colisiones de nombres en bases grandes.
- Angular Aria: las APIs de accesibilidad ya dejaron de estar en preview. Si te tomás en serio la a11y, esto te suma.
- Bastante laburo de seguridad: el paquete platform-server ahora protege contra SSRF y path hijacking, rechaza URLs sospechosas y cierra bypasses en HttpClient. Lo bueno es que la mayoría de esos fixes te caen solos al actualizar, sin que toques nada.
¿Vale la pena?
Mi opinión después de tenerlo un finde entre manos: sí, me convenció. No lo sentí como una lista de features sueltas, sino como que la forma de escribir componentes, formularios y llamadas a la API ahora gira toda alrededor de signals, y eso en el día a día se nota.
Si arrancás un proyecto nuevo, yo lo haría zoneless y signal-first desde el día uno. Si tenés una app grande andando, la buena noticia es que ng update te cubre la espalda con Eager y podés modernizar de a poco — componente por componente, formulario por formulario.
Lo que más me gustó es que dejé de ver el cartelito de "experimental" en las cosas que quería usar. Pude meter signals en código real sin esa sensación de estar probando algo que capaz mañana cambia. No sé si será para todos, pero a mí me dieron ganas de arrancar mi próximo proyecto con esto sí o sí.
Fuentes
- Blog oficial de Angular: Announcing Angular v22
- Angular Architects: Angular 22 — The Most Important New Features at a Glance
- dev.to: Angular 22 Is Here — Everything You Need to Know