Angular 22: The End of Boilerplate and the Consolidation of the Reactive Era
If you have been following the evolution of Google’s framework over the last few years, you know it has been undergoing a silent reconstruction - piece by piece. With the release of Angular 22 on June 3, 2026, this reconstruction is no longer a promise and has become the standard.
We are not looking at another batch of experimental features: we are looking at the consolidation of an entirely rethought ecosystem. For those who live and breathe enterprise applications, Clean Architecture, and Microfrontend ecosystems, this is the version that finally delivers what has been promised since Angular 16: an end-to-end reactive framework, zone-less by nature, and with much less ceremony along the way.
The experiments are over. Below is what has actually changed - and what you need to do before running ng update.
📖 If you are just starting out: several technical terms in this article (change detection, Signals, SSR, dependency injection, microfrontends...) are explained in a glossary at the end. Read the article from end to end and use the glossary as a reference whenever you have a doubt.
What Arrived in Angular 22
- OnPush is the new default change detection (the old
DefaultbecameEagerand is deprecated). - Stable Resource API:
resource,rxResource, andhttpResourceare ready for production. - Stable Signal Forms: featuring the Submission API, dynamic schemas (Zod/Valibot), and interop with Reactive Forms.
- New
@Service()decorator: shortening@Injectable({ providedIn: 'root' }). injectAsync: for lazy dependency injection, with prefetch viaonIdle.debounced: for native debounce in Signals/Resources.- Incremental Hydration: enabled by default.
HttpClient: uses FetchBackend by default (withFetch()is deprecated).- Important Router and bootstrap improvements designed for Microfrontends.
1. OnPush as the New Default Change Detection
The moment the community has always asked for has arrived: ChangeDetectionStrategy.OnPush is now the default behavior for any new component.
This decision makes perfect sense in a signals-first world - those who use Signals already receive surgical notifications about what changed, and OnPush takes full advantage of this, checking only the truly affected components instead of scanning the entire tree.
The old Default (which checked the whole tree) was renamed to Eager and is deprecated. If you still need the old behavior in a component, declare it explicitly:
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'app-legacy',
changeDetection: ChangeDetectionStrategy.Eager, // replaces the old 'Default'
template: `...`,
})
export class LegacyComponent {}
Critical detail for the migration: during ng update, if Angular doesn’t find an explicit strategy, it automatically applies Eager so nothing breaks. That is, you don’t get performance “for free” - you need to migrate component by component to reap the benefits of OnPush.
2. Stable Resource API and httpResource
The Resource API was the missing piece in the Signals puzzle: deriving asynchronous data reactively, usually by triggering HTTP requests when a Signal changes. Now resource, rxResource, and httpResource are stable and cleared for production.
The most comfortable entry point is httpResource. It takes a reactive lambda that returns the request: if a Signal used inside it changes, the request is automatically re-fired.
import { httpResource } from '@angular/common/http';
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { Flight } from './flight';
@Component({
selector: 'app-flight-search',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@if (flightsResource.isLoading()) {
<div>Loading…</div>
} @else if (flightsResource.error()) {
<div>Error: {{ flightsResource.error() }}</div>
} @else {
@for (flight of flightsResource.value(); track flight.id) {
<app-flight-card [item]="flight" />
}
}
`,
})
export class FlightSearch {
protected readonly filter = signal({
from: 'São Paulo',
to: 'Uberlândia',
});
protected readonly flightsResource = httpResource<Flight[]>(
() => ({
url: 'https://api.example.io/flight',
params: {
from: this.filter().from,
to: this.filter().to,
},
}),
{
defaultValue: [], // prevents the component from dealing with `undefined` on initialization
},
);
protected reload(): void {
this.flightsResource.reload();
}
}
The resource manages its own state via Signals: value, error, isLoading, and a more detailed status (idle, loading, reloading, error, resolved, local).
And the best part: race conditions are handled automatically - if several requests arrive in sequence, only the result of the most recent one is used, exactly like switchMap would do in RxJS, but without you writing a single line of pipe.
Want to skip the request under certain conditions? Just return undefined in the lambda.
3. Signal Forms Ready for Production
The eternal divide between Reactive Forms and Template-Driven is over. Signal Forms have left experimental status and are the recommended approach for forms: declarative, strongly typed, and reactive via Signals.
The heart of the API is the form function, which receives a Signal with the data and a validation schema:
import { form, minLength, required } from '@angular/forms/signals';
protected readonly flightForm = form(this.flight, (path) => {
required(path.from);
required(path.to);
required(path.date);
minLength(path.from, 3);
});
The result is a FieldTree: a nested structure of Signals where each field exposes value, dirty, invalid, and errors.
In the template, you use the FormField directive:
<input [formField]="flightForm.from" id="flight-from" />
<div>{{ flightForm.from().errors() | json }}</div>
And it doesn’t stop there. Angular 22 (adding to 21.1 and 21.2) brought a surprisingly complete forms stack:
- Submission API (
FormRoot+submit): all submission logic inside the form itself, including mapping validation errors from the server back into the form state. - Dynamic schemas with
validateStandardSchema, compatible with Zod and Valibot - and which re-evaluate when a Signal changes. - Conditional CSS classes (
ng-valid,ng-invalid,ng-dirty…) viaprovideSignalFormsConfig. - Interop with Reactive Forms via
compatFormandSignalFormControl, allowing you to migrate incrementally without rewriting the world.
4. The New @Service() Decorator
One of those ergonomic improvements you’ll be thankful for every day. @Service() shortens the most common injection case - that endlessly repeated @Injectable({ providedIn: 'root' }):
import { Service } from '@angular/core';
@Service()
export class FlightClient {
// Provided in root by default. The intention is explicit.
}
If you don’t want the automatic root provision, turn it off with autoProvided: false and provide it manually (in app.config.ts, in the component, or in the route):
@Service({ autoProvided: false })
export class TabRegistry {}
Important: @Service() does not retire @Injectable(). It is a shortcut for the most frequent case. Wherever you use more sophisticated provider configurations, @Injectable() remains the right tool.
In a Clean Architecture, @Service() makes the infrastructure adapter layer much leaner - but use it with discretion, not as a blind substitute.
5. injectAsync: Lazy Dependency Injection
This is a gift for anyone fighting bundle sizes and startup times. With injectAsync, you inject a dependency only when it is truly needed - ideal for services that load heavy libraries and only step in after a specific user action:
import { injectAsync } from '@angular/core';
@Component({ /* ... */ })
export class CheckinPage {
private readonly upgradeService = injectAsync(() =>
import('./upgrade-service').then((m) => m.UpgradeService),
);
protected async upgrade(): Promise<void> {
const service = await this.upgradeService();
service.upgrade(/* ... */);
}
}
The import
Comments
No comments yet. Start the discussion.