Angular v22 is a major release, but it won't ruin your week. Instead of dropping a pile of new APIs on you, the team took a long list of experimental features and marked them stable, then flipped a few defaults to match how people actually write Angular these days. If you've already moved to signals, standalone components, and the new control flow, a lot of v22 feels less like learning something new and more like the framework catching up to your code.
Let's go through it section by section, starting with the change that touches every component you'll ever write: a new default change detection strategy.
Angular v22 at a Glance
- Release date: June 3, 2026
- OnPush is now the default change detection strategy for every component
- Signal Forms, the resource API, and Angular Aria are stable
- Vitest replaces Karma and Jasmine as the default test runner
- Incremental hydration is on by default for server-side rendered apps
- HttpClient uses the Fetch API instead of XMLHttpRequest by default
- New
@Servicedecorator andinjectAsync()for dependency injection - Requirements changed: TypeScript v6 and Node v22 are now mandatory
OnPush Is the New Default Change Detection Strategy
This is the one to pay attention to. For years, every component you created ran with the old
"check everything, all the time" strategy, which re-runs a component's bindings on almost any event anywhere in
the app. From v22 on, a component that doesn't declare a strategy gets ChangeDetectionStrategy.OnPush
instead. Angular re-checks it only when its inputs change, a signal it reads updates, or an event fires inside it.
Nothing more.
If your app already leans on signals and immutable inputs, you get fewer wasted checks and faster rendering for free, without touching a line. The catch is older code that quietly mutates objects in place: those views can stop updating. The good news is the migration handles it for you, dropping in the opt-out strategy wherever it's needed:
import { ChangeDetectionStrategy, Component } from '@angular/core';
// New behavior in v22: OnPush is implied if you write nothing.
@Component({
selector: 'app-product',
templateUrl: './product.html'
})
export class ProductComponent {}
// Opt back into the old "check always" behavior when you need it.
@Component({
selector: 'app-legacy',
changeDetection: ChangeDetectionStrategy.Eager,
templateUrl: './legacy.html'
})
export class LegacyComponent {}
Notice the renamed value: the old default goes by ChangeDetectionStrategy.Eager
now. When you run ng update,
the schematic stamps it onto every component that didn't already set a strategy, so nothing changes behavior-wise
on day one. From there you can peel it off one component at a time, checking each view still works under OnPush as
you go.
Signal Forms Are Stable
Signal Forms finally shed the experimental label in v22. The whole API is built on signals, which means the
form's value, its validation state, even whether a field has been touched, are all reactive signals you can read
straight from a template or a computed. No FormGroup, no FormControl tree to babysit
and keep in sync with your model.
You start with a plain object model wrapped in signal(), then describe how
it should validate with the form()
function and a schema:
import { Component, signal } from '@angular/core';
import { form, required, email, minLength } from '@angular/forms/signals';
@Component({
selector: 'app-login',
templateUrl: './login.html'
})
export class LoginComponent {
model = signal({ email: '', password: '' });
loginForm = form(this.model, (path) => {
required(path.email);
email(path.email);
required(path.password);
minLength(path.password, 8);
});
submit() {
if (this.loginForm().valid()) {
console.log(this.model());
}
}
}
In the template you bind fields with the Field directive and read
errors straight from the form signal:
<form (submit)="submit()">
<input type="email" [field]="loginForm.email" />
<input type="password" [field]="loginForm.password" />
<p>{{ loginForm.password().getError('minLength') }}</p>
<button [disabled]="loginForm().invalid()">Sign in</button>
</form>
The stable release sprinkles in the things you actually reach for day to day: minDate() and maxDate() validators, a
getError() method that
narrows types properly, debouncing on blur and on async validators, and a touch() output that retires
the awkward mutable touched flag. And if you've got custom controls written the old way with ControlValueAccessor, they
keep working with Signal Forms, so you don't have to rewrite everything at once to adopt this.
The resource API Is Production-Ready
resource(), rxResource(), and httpResource() are stable
as
of v22. Their whole point is to keep async data living inside the signal graph, so you stop hand-stitching RxJS
and signals together every time you fetch something. An httpResource refetches on
its own whenever a signal it depends on changes, and it hands you loading, error, and value as signals you can
read directly.
import { Component, signal } from '@angular/core';
import { httpResource } from '@angular/common/http';
@Component({
selector: 'app-user',
templateUrl: './user.html'
})
export class UserComponent {
userId = signal(1);
// Refetches automatically whenever userId() changes.
user = httpResource(() => `/api/users/${this.userId()}`);
}
@if (user.isLoading()) {
<p>Loading...</p>
} @else if (user.error()) {
<p>Something went wrong.</p>
} @else {
<h2>{{ user.value()?.name }}</h2>
}
Two things in the stable release are worth calling out. There's a new chain() method that makes
feeding one resource's result into the next painless, with loading and error states propagating on their own. And
if you pass an id in
the
options, a resource resolved during server-side rendering gets cached and shows up instantly on the client, no
second loading flash while it refetches.
The @Service Decorator and injectAsync()
Spinning up a singleton service has always meant typing out @Injectable({ providedIn: 'root' }).
The new @Service()
decorator says the exact same thing with a lot less ceremony. Out of the box it gives you a tree-shakeable,
app-wide singleton, same as the root-provided Injectable, except now the name tells you what it is:
import { Service, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Service()
export class ProductService {
private http = inject(HttpClient);
getProducts() {
return this.http.get('/api/products');
}
}
One thing to know: a @Service
pulls its dependencies through inject() rather than the
constructor, which lines it up with how the rest of modern Angular already works.
Sitting right next to it is injectAsync(), which
lazy-loads a service through a dynamic import so its code ships in its own chunk and only downloads the first time
someone needs it. If you'd rather not pay that cost on demand, you can have it prefetch quietly while the browser
is idle:
import { injectAsync, onIdle } from '@angular/core';
export class CheckoutComponent {
private heavyService = injectAsync(
() => import('./pricing.service').then((m) => m.PricingService),
{ prefetch: onIdle }
);
}
Vitest Replaces Karma and Jasmine
New projects in v22 ship with Vitest as the test runner. Karma has been on its way out for a while, and Jasmine is no longer what you get by default. Vitest is quick, runs in watch mode without you asking, and reuses the same build config your app already has, so you're not maintaining a separate toolchain just for tests.
Existing projects keep running as-is, and the CLI gives you schematics to make the jump. To convert a Karma and Jasmine setup, run:
ng generate @schematics/angular:migrate-karma-to-vitest
Worried about your async tests? If you lean on fakeAsync, flush, or waitForAsync, those Zone.js
helpers run under Vitest now through a dedicated patch, so there's nothing to rewrite. A couple of new flags help
too: --isolate runs
tests in separate processes, and --quiet keeps build noise
out of your output.
Incremental Hydration by Default
If you render on the server, incremental hydration is on by default now. Rather than waking up the whole page in
one shot, Angular hydrates pieces of it as they scroll into view or as the user interacts with them. That means
less JavaScript running on first load and better interaction numbers on heavy pages. The old withIncrementalHydration()
call is deprecated, since you get the behavior for free. While we're here: provideServerRendering()
picked up a maxResponseBodySize option
that defaults to 1 MB.
HttpClient Switches to the Fetch API
Under the hood, HttpClient now talks to the browser Fetch API instead of XMLHttpRequest. The withFetch() you used to opt
into is simply the default, and if something in your stack still needs the old transport you can switch back with
withXhr(). Fetch keeps
Angular aligned with the modern platform, behaves better outside the browser, and gets along nicely with
server-side rendering.
import { provideHttpClient, withXhr } from '@angular/common/http';
export const appConfig = {
providers: [
// Fetch is the default now; opt out only if you must.
provideHttpClient(withXhr())
]
};
Template and Compiler Improvements
Templates keep getting more capable and stricter at the same time. The change you'll notice first is being able to drop an HTML comment inside an opening tag, which is genuinely handy when you want to annotate or temporarily kill a single attribute on a long element:
<input
type="text"
[value]="name()"
<!-- [disabled]="locked()" temporarily off -->
(input)="onInput($event)"
/>
Beyond that, the compiler got smarter about optional chaining with automatic null guards, added exhaustive @switch checks, gave @defer (on idle(500ms)) a
timeout so it can't wait forever, and now catches duplicate input or output names and broken @for loops at build time
instead of letting them slip through to runtime. Strict templates are on by default too, and the migration adds
strictTemplates: false
for any project that isn't ready for that yet.
AI Integration with WebMCP
v22 also takes an experimental swing at AI with WebMCP, a browser protocol that lets AI agents call into your running app. The part that caught my eye is automatic form exposure: wire up the providers, tag a Signal Form, and Angular reads the form's model, builds a JSON schema from it, and makes the whole thing callable by an agent. The agent even gets the validation errors back and can fix its own input.
import { provideExperimentalWebMcpForms } from '@angular/forms/signals';
export const appConfig = {
providers: [
provideExperimentalWebMcpForms()
]
};
The team also shipped official Angular skills you can bolt onto AI coding tools with npx skills add https://github.com/angular/skills,
and your signal and dependency injection graphs now show up as Chrome DevTools extensions. All of this is
experimental, so don't be surprised when the API shifts around before it settles.
Angular Aria Is Stable
The @angular/aria
package made it from developer preview to production-ready. It hands you accessible building blocks, things like
keyboard navigation and the ARIA wiring nobody enjoys writing by hand, that you compose into your own components,
and it plugs straight into Signal Forms. If you maintain a design system in-house, this is the accessibility
groundwork without dragging in a whole component library to get it.
Security Hardening
There's a real security story here too. v22 added server-side request forgery (SSRF) protections around how platform-server sets up
location and document, tightened sanitization for dynamic href attributes on SVG
anchors, and hardened locale data against prototype pollution. The transfer cache now skips any request carrying
cookies or using withCredentials
by default, so sensitive responses don't get baked into the page. None of this asks anything of you, but it's nice
to know your app comes out the other side of the upgrade a little safer.
Breaking Changes to Watch
The migrations cover most of v22, but a handful of changes are worth eyeballing yourself before you ship:
- TypeScript v6 and Node v22 are required. TypeScript 5.9 and Node 20 are no longer supported.
- OnPush is the default. Views that relied on mutating objects in place may stop updating
until you switch them to
Eageror move to signals. - Router parameter inheritance changed.
paramsInheritanceStrategynow defaults to'always', and this one is not auto-migrated. - Optional chaining returns
undefinedinstead ofnullin templates; a migration can preserve the old behavior if you depend on it.
How to Upgrade to Angular v22
Before anything else, get yourself onto Node v22 and make sure the project is already on Angular v21, since the CLI only jumps one major version at a time. Then run the update:
# Check your current version
ng version
# Update the core framework and CLI to v22
ng update @angular/core@22 @angular/cli@22
The schematics bump TypeScript, drop ChangeDetectionStrategy.Eager
in where it's needed, and clean up deprecated APIs along the way. Once it's done, run your tests, keep an eye out
for views that stopped updating under OnPush, and double-check any code that pulled router parameters from a
parent route still does what you expect. When everything's green, start pulling those Eager markers back out so
you actually get the benefit of the new default.
Conclusion
v22 isn't really about shiny new toys. It's about making the modern, signal-first way of building Angular the path
of least resistance. OnPush by default pays you back for writing reactive code. Stable Signal Forms and the
resource API take away the last few reasons to reach for the older patterns. Vitest, incremental hydration, and
the Fetch-based HttpClient quietly modernize the tooling underneath you. And @Service trims a little
boilerplate off every service you write.
The upgrade is mostly automated, the breaking changes are manageable, and what you get out of it is a faster app with cleaner code. If you're already writing signals and standalone components, v22 is going to feel like home. When you're ready to dig into the specifics, the official Angular v22 release page and the Angular update guide are the two links to keep open.