I spent a weekend actually trying Angular 22 — not just skimming the changelog. And honestly, it feels different from the last few releases. It's not just another version bump: a bunch of things I'd been eyeing from a distance precisely because they said "experimental" — signals, zoneless, signal-based forms — I could finally use this time without that label on them. And for me that changes how I approach a new project quite a bit.
Angular 22 shipped on June 3, 2026. Here's what stood out to me after getting hands-on, with code examples from what I tried.
OnPush is now the default (and it makes total sense)
This is the change I liked most, even if it throws you off at first. Forever, every new Angular component used the Default change detection strategy, which checks the whole tree on any change. Now the default is OnPush.
In a world where you write with signals, this fits perfectly: a component only re-renders when a signal or input it actually uses changes — not when something moves somewhere else in the app.
If you need the old behavior, you ask for it explicitly. What used to be Default is now called Eager (and it's deprecated):
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'app-legacy',
changeDetection: ChangeDetectionStrategy.Eager, // replaces the old 'Default'
template: `...`,
})
export class LegacyCmp {}The nice part: if you update an existing project, ng update sets Eager on components that didn't have OnPush explicitly set, so nothing breaks. But new code gets the modern default out of the box.
httpResource: reactive fetching without subscribe
This is the one that made me go "ah, finally." The Resource API (resource, rxResource, and httpResource) left experimental behind.
httpResource takes a function that returns an HTTP request. And here's the key: that function is reactive. If a signal you use inside it changes, the request automatically re-fires. No subscribe, no async pipe, no managing unsubscribes.
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;
}In the template you consume it directly, no weird pipes:
@if (flightsResource.isLoading()) {
<div>Loading...</div>
}
@if (flightsResource.error()) {
<div>Error: {{ flightsResource.error() }}</div>
} @else {
@for (flight of flightsResource.value(); track flight.id) {
<app-flight-card [item]="flight" />
}
}What sealed it for me: if several requests land back-to-back (classic when the user types fast in a filter), it keeps the latest and cancels the older ones, without you doing anything. Anyone coming from RxJS knows this as switchMap behavior; here it's just handled. And the defaultValue saves you from dealing with undefined at startup.
Signal Forms: forms without the usual boilerplate
If you ever suffered with FormGroup and FormControl, you'll like this section. Signal Forms are usable for real now.
The heart of it is the form() function: you pass it a signal with the data and a validation schema. Each form property ends up being a signal with its own status (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);
});
}And in the template you bind it with the formField directive. Much more direct than before:
<input [formField]="flightForm.from" id="flight-from" />
<div>{{ flightForm.from().errors() | json }}</div>The first time I built a form like this I realized I didn't miss anything about the old model: no more three-story nested FormGroups.
The @Service decorator
A small but elegant detail. How many times have you written @Injectable({ providedIn: 'root' }). Now there's a shortcut that says what you actually mean:
import { Service } from '@angular/core';
// Provided in root by default
@Service()
export class FlightClient {}
// If you DON'T want it auto-provided
@Service({ autoProvided: false })
export class TabRegistry {}It's one of those things that won't change your life, but every time you use it you think "nice that this reads like this now."
injectAsync and debounced: the details that feel good
Two new primitives that struck me as very practical.
injectAsync lets you inject dependencies lazily — only when they're actually needed. Perfect for services that load heavy libraries and are only used on a specific user action:
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();
}
}The ReportService bundle only loads the first time you call generateReport(). Combined with lazy routes, it gives you incredibly fine-grained control over what loads and when. And if you want to get ahead of it, there's a prefetch: onIdle option to preload when the browser is free.
debounced is tiny but I'll use it a lot. It wraps a signal and delays its notifications — exactly what you want for a search input, without reaching for RxJS debounceTime again:
import { debounced, signal } from '@angular/core';
const searchTerm = signal('');
const debouncedTerm = debounced(searchTerm, 300); // 300ms
// Use debouncedTerm in an httpResource and you're done:
// it won't fire a request on every keystroke.Zoneless for real (goodbye Zone.js)
For most apps you no longer need Zone.js. Change detection is explicit and efficient, triggered by signals, events, or manual calls. That shrinks the bundle, makes debugging saner, and brings Angular closer to native JavaScript performance.
You enable it at bootstrap:
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));One thing to watch: in zoneless, changes that happen outside Angular (a setTimeout, browser events) won't trigger change detection on their own unless you wrap them in Angular APIs — but if you're already working with signals, that's exactly what you want.
The rest worth mentioning
Not everything deserves its own section, but several things add up:
- Vitest is the default runner in new projects, instead of Karma. Tests start up faster and it feels more modern.
- Incremental hydration by default:
provideClientHydration()now enables it automatically (it hydrates components as they become visible, improving Time to Interactive). If you don't want it, opt out withwithNoIncrementalHydration(). - Selectorless components (in preview): you can import a component and use it in the template without a string selector, avoiding name collisions in large codebases.
- Angular Aria: the accessibility APIs left preview. If you take a11y seriously, this is a win.
- A good chunk of security work: the platform-server package now guards against SSRF and path hijacking, rejects suspicious URLs, and closes bypasses in HttpClient. The good part is most of those fixes land on their own when you update, without you touching anything.
Is it worth it?
My take after having it in my hands for a weekend: yes, it won me over. I didn't experience it as a list of loose features, but as the way you write components, forms, and API calls now revolving entirely around signals — and that shows in day-to-day work.
If you're starting a new project, I'd go zoneless and signal-first from day one. If you have a large app running, the good news is ng update has your back with Eager, and you can modernize gradually — component by component, form by form.
What I liked most is that I stopped seeing the "experimental" tag on the things I wanted to use. I could put signals into real code without that feeling of trying something that might change tomorrow. I don't know if it's for everyone, but it made me want to start my next project with this, no question.
Sources
- Official Angular blog: 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