What's New in Angular v22: The Signal-First Release
Angular v22 makes signals the default: OnPush out of the box, stable Signal Forms, stable resource/httpResource, big template upgrades, @Service, and Angular Aria. A practical tour with code.
Angular v22 is the release where the signal-first vision stops being "the future" and becomes the default. Several APIs that were experimental or in developer preview are now stable, the templates got noticeably more expressive, and new applications are zoneless with OnPush out of the box. Here's a practical tour of what actually changed - and what it means for how you write Angular day to day.
TL;DR - what's new in Angular v22:
OnPushis the default for new components (ChangeDetectionStrategy.Defaultwas renamed toEager).- Signal Forms (
@angular/forms/signals) are stable - type-safe, declarative, noControlValueAccessor. resource()/rxResource()/httpResource()are stable - async data as signals, noswitchMapplumbing.- Templates gained spread/rest syntax, stacked
@switchwith exhaustive@default never;, and inline arrow functions in event bindings. @Service()decorator +injectAsync()for cleaner singletons and lazy, code-split DI.- Angular Aria (
@angular/aria) is stable - headless, accessible UI primitives. - New apps are zoneless (default since v21) - change detection runs when a signal changes.
1. OnPush is the default (and Default was renamed to Eager)
New components now use OnPush change detection by default. This aligns new apps with the zoneless model that became the default in v21, and it means change detection runs only when a component's inputs change or one of its signals updates.
The previous, eager strategy is still available - it was simply renamed to make its behavior obvious:
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'app-weather',
template: `<section>Loading weather…</section>`,
// OnPush is now the default, so you rarely need to set this.
// The old `ChangeDetectionStrategy.Default` is now `Eager`:
changeDetection: ChangeDetectionStrategy.Eager,
})
export class WeatherComponent {}
If you ng update an existing project, Angular keeps your components on Eager so nothing breaks - the new default applies to newly generated components.
2. Signal Forms are stable
@angular/forms/signals graduated to stable in v22. You define your form state as a signal, call form() to get a typed form descriptor, and bind fields with the [formField] directive - combining the type-safety of reactive forms with the ergonomics of template-driven ones.
import { Component, signal } from '@angular/core';
import { form, FormField, required } from '@angular/forms/signals';
@Component({
selector: 'app-checkout',
imports: [FormField],
template: `
<select [formField]="checkoutForm.method">
<option value="">Choose a method…</option>
<option value="credit">Credit Card</option>
<option value="paypal">PayPal</option>
</select>
@if (checkoutForm.method().invalid() && checkoutForm.method().touched()) {
<p class="error">A payment method is required.</p>
}
`,
})
export class CheckoutComponent {
model = signal({ method: '' });
checkoutForm = form(this.model, (schema) => {
required(schema.method, { message: 'Please select a payment method.' });
});
}
Validity, errors, and values are all signals - no ControlValueAccessor boilerplate, and full compile-time type safety.
3. resource(), rxResource(), and httpResource() are stable
The async reactivity APIs are now production-ready. httpResource() is the most convenient entry point: give it a reactive loader that returns a URL (or request), and it manages loading/error/value as signals - re-running automatically when a signal it reads changes.
import { httpResource } from '@angular/common/http';
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-weather',
template: `
@if (weather.isLoading()) { <p>Loading…</p> }
@else if (weather.error()) { <p>Failed to load.</p> }
@else if (weather.value(); as w) { <p>{{ w.temperature }}°</p> }
`,
})
export class WeatherComponent {
city = signal('Stockholm');
weather = httpResource<{ temperature: number }>(
() => `/api/weather/${this.city()}`
);
}
Change city, and the request re-runs - no switchMap, no manual loading flags. For RxJS sources, rxResource() does the same with an observable stream.
4. Templates got more expressive
Three template upgrades stand out in v22.
Spread/rest syntax now works in templates - in object literals, array literals, and even function calls:
<div [class]="{ ...baseStyles, active: isSelected() }"></div>
<app-list [items]="[...cached(), 'new-item']" />
<p>Total: {{ calculateBill(...basePrices(), taxRate()) }}</p>
Stacked @switch cases + exhaustive checking with @default never; - the compiler fails the build if you add a union member and forget to handle it:
@switch (status()) {
@case ('pending')
@case ('processing') { <span>In progress</span> }
@case ('done') { <span>Done</span> }
@default never;
}
Inline arrow functions are now valid in template event bindings, so simple mutations no longer need a helper method:
<button (click)="count.update(c => c + 1)">Add</button>
(Keep these short - push anything non-trivial back into the component class.)
5. @Service() and asynchronous DI with injectAsync()
The new @Service() decorator is a cleaner way to declare an application-wide singleton - no @Injectable({ providedIn: 'root' }) wrapper needed:
import { Service } from '@angular/core';
@Service()
export class PaymentService {
processPayment(method: string, amount: number) { /* … */ }
}
And injectAsync() lets you lazy-load heavy dependencies on demand, returning a loader you await when you actually need the service - handy for code-splitting large, rarely-used features.
6. Angular Aria is stable
@angular/aria graduated to stable: a set of headless, fully accessible directives (accordions, tabs, menus, listboxes, trees) that handle keyboard navigation and ARIA semantics for you, while you keep full control of the styling. Because it's signal-based under the hood, it integrates cleanly with component state and Signal Forms.
Should you migrate now?
You don't have to rewrite anything. Existing apps keep working (your components stay on Eager, RxJS keeps running). The pragmatic path is incremental: adopt signals for new state, lean on httpResource for new data fetching, and migrate RxJS/NgRx hotspots where the simplification pays off. Signals and RxJS interoperate cleanly via toSignal/toObservable, so both can coexist while you transition.
Go deeper
Want the migration playbook in detail? Start with RxJS to Angular Signals: A Practical Migration Guide.
For the complete, hands-on path - every API above with runnable examples, a dedicated RxJS/NgRx migration chapter, testing strategies, and performance/zoneless best practices - my book Mastering Angular Signals is fully updated for v22, with a foreword from the Angular team at Google. Get it on Leanpub (DRM-free PDF/EPUB) - also available on Amazon (Kindle and paperback). Already own it on Leanpub or Kindle Unlimited? The v22 update is free - just re-download.
FAQ
Is Angular v22 zoneless by default?
Zoneless became the default for new applications in v21, and v22 builds on it by making OnPush the default change detection strategy for new components. Existing apps can still use Zone.js.
Are Signal Forms stable in Angular v22?
Yes. @angular/forms/signals (the form() function, the [formField] directive, and validators like required) is stable and production-ready in v22.
What replaced ChangeDetectionStrategy.Default?
It was renamed to ChangeDetectionStrategy.Eager. OnPush is now the default for new components; ng update keeps existing components on Eager.
Do I have to migrate my RxJS code to signals?
No. RxJS still works and remains the right tool for event streams. Migrate incrementally where signals simplify state, using toSignal/toObservable at the boundaries.