TLDR summary
- Signal Forms and Resource API are now production-stable, ending the experimental phase that started in Angular 21
- OnPush is the new default change detection strategy, requiring an explicit Eager strategy for legacy behavior
- @Service decorator replaces verbose @Injectable({ providedIn: 'root' }) patterns with cleaner, more intentional syntax
- injectAsync enables lazy dependency injection with optional prefetching, reducing bundle size and startup time
- WebMCP integration allows AI agents to call your application and forms directly, opening new automation possibilities
The release that changes everything
Angular 22 officially launched on June 3, 2026, and this release carries more weight than a typical major version. This isn't a release built around one headline feature. This is the release where three years of investment in signals, accessibility, and reactive architecture stop being "coming soon" and become your daily reality.
If you've been holding off on upgrading because Signal Forms were experimental or the change detection defaults felt risky, Angular 22 removes every reason to wait. The framework has matured. The features you heard about in Angular 21 are now battle-tested, production-ready, and in many cases, the default behavior.
What this means for your team
The core difference is that Angular 22 turns v21's experiments into stable, on-by-default behavior. Most of what you read about in the v21 release notes as "coming" or "try this" is now the recommended path. For teams already on Angular 21, the upgrade feels like a refinement rather than a migration. But there are a few behavioral changes worth understanding, and several performance wins waiting for you.
Feature 1: Signal forms go stable (Production-Ready)
Signal Forms were promoted to production-ready status in Angular 22 after feedback from teams inside and outside Google confirmed the team was on the right track. This is the most significant forms API update since Reactive Forms were introduced.
Why Signal forms matter
Traditional Reactive Forms built on RxJS observables work well, but they introduce layers of indirection. You create FormGroups, wrap FormControls, subscribe to value changes, and manage subscriptions. Signal Forms eliminate this friction by treating forms as first-class signals that automatically propagate changes without explicit subscription logic.
Signal Forms unify reactive forms, template-driven development, strong typing, and signal-based reactivity in a single cohesive API.
From Reactive forms to Signal forms
Here's what the migration looks like in practice. Traditional approach:
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { Component } from '@angular/core';
@Component({
selector: 'app-user-form',
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="email" type="email" />
<input formControlName="name" type="text" />
<button type="submit" [disabled]="!form.valid">Save</button>
</form>
`,
standalone: true,
imports: [ReactiveFormsModule]
})
export class UserFormComponent {
form: FormGroup;
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
name: ['', Validators.required]
});
}
onSubmit() {
if (this.form.valid) {
console.log(this.form.value);
}
}
}
Signal Forms approach:
import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
@Component({
selector: 'app-user-form',
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="email" type="email" />
<input formControlName="name" type="text" />
<button type="submit" [disabled]="!form.valid.value">Save</button>
<p *ngIf="form.errors.value">Form has errors</p>
</form>
`,
standalone: true,
imports: [CommonModule]
})
export class UserFormComponent {
form = new FormGroup({
email: new FormControl('', [Validators.required, Validators.email]),
name: new FormControl('', Validators.required)
});
onSubmit() {
if (this.form.valid.value) {
console.log(this.form.getRawValue());
}
}
}
The key difference: form validity, error state, and value changes are now signals (.value suffix) that automatically trigger change detection. No subscription boilerplate. No manual teardown. The form is a reactive object that the template binds to directly.
Advanced: Typed Signal Forms
Signal Forms in Angular 22 also introduce first-class typing for form values:
interface User {
email: string;
name: string;
age: number;
}
const userForm = new FormGroup({
email: new FormControl<string>(''),
name: new FormControl<string>(''),
age: new FormControl<number>(0)
});
// Now userForm.value is typed as { email: string; name: string; age: number }
// TypeScript catches errors before runtime
const user: User = userForm.getRawValue();
Migration path
For existing components without an explicit strategy, ng update automatically sets ChangeDetectionStrategy.Eager to preserve the previous behavior, so you won't have breaking changes on day one. You can migrate forms incrementally, adopting Signal Forms in new components while keeping Reactive Forms in legacy code.
Common pitfall: Don't try to migrate all forms at once. Pick high-value, low-complexity forms first (login, filters, search). This builds confidence before tackling complex multi-step wizards.
Feature 2: OnPush is now default (performance win)
OnPush is the new default change detection strategy, making perfect sense with the rise of Signals and Zone-less Angular.
What changed and why It matters
In Angular's old model, components used "Eager" (formerly "Default") change detection. Every time anything changed in your app, Angular checked every component tree. This worked, but it's inefficient at scale. OnPush change detection only checks a component if its inputs changed or if an event fired from within that component.
In a Signals-first world, anyone using Signals gets precise notifications about changes, and OnPush takes full advantage of that by focusing change detection only on the components actually affected.
The default behavior changed silently
Before Angular 22:
@Component({
selector: 'app-card',
template: `<h2>{{ title }}</h2>`,
changeDetection: ChangeDetectionStrategy.OnPush // You had to opt in
})
export class CardComponent {
@Input() title: string;
}
Angular 22 and later:
@Component({
selector: 'app-card',
template: `<h2>{{ title }}</h2>`
// OnPush is now the default - no need to specify it
})
export class CardComponent {
@Input() title: string;
}
If you need the old behavior:
@Component({
selector: 'app-legacy',
template: `<div>{{ data }}</div>`,
changeDetection: ChangeDetectionStrategy.Eager // Explicitly opt out
})
export class LegacyComponent {
data = this.mutateDirectly(); // Anti-pattern, but still works
}
Performance impact
For large applications with hundreds of components, this change reduces change detection cycles dramatically. A dashboard with 50+ cards, only 3 of which actually changed, will now skip checking 47 cards entirely. Over a session with thousands of interactions, this compounds into measurable improvements in frame rates and responsiveness.
Real-world example: An e-commerce product listing with 100 cards. In Eager mode, every time you hover over any card, Angular checks all 100. In OnPush mode, Angular checks only the one you hovered over. The performance difference is obvious on older devices.
Breaking change mitigation
The ng update command handles this automatically. Components that don't explicitly set changeDetection will get ChangeDetectionStrategy.Eager added automatically by the migration. This preserves behavior while leaving a clear "to-do" marker in your code for future refactoring. You can then remove these markers one by one, confident that each component is actually compatible with OnPush.
Feature 3: @Service Decorator (Cleaner Dependency Injection)
The @Service decorator is now stable and provides a new, more intentional way to declare services as root-provided singletons.
The problem It solves
Before Angular 22, providing a singleton service at the root level required verbose, non-obvious syntax:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class ApiService {
// This config is the most common case, but you have to remember the boilerplate
}
The term "Injectable" describes a mechanism, not what the class is. Developers often forgot the configuration and ended up with service instances scattered across different injection scopes.
The New Way: @Service
import { Service } from '@angular/core';
@Service()
export class ApiService {
// Default: provided in root as a singleton
// Clear, readable, intentional
}
That's it. The decorator name describes the thing (it's a service), and the default behavior is the most common case (root-level singleton).
Optional: Scoped Services
The @Service decorator can be configured to opt out of auto-providing, giving developers explicit control when they need services scoped to specific feature modules or components.
@Service({ autoProvided: false })
export class TabRegistry {
// This service won't be automatically provided
// You can manually provide it in a module or component provider array
// Useful for services that manage local state
}
Migration Impact
If you're running a large codebase with 50+ services, you don't need to migrate everything immediately. The old @Injectable({ providedIn: 'root' }) syntax still works. But for all new services, use @Service(). Over time, your codebase will naturally converge toward the cleaner approach.
The real benefit isn't just syntax reduction. It's consistency. Every developer on the team will instantly recognize that @Service() means "root singleton" without having to remember boilerplate patterns.
Feature 4: injectAsync for Lazy Dependency Injection
injectAsync enables lazy dependency injection with optional prefetching on idle time, allowing services to be loaded on-demand rather than at startup.
When you need this
Large applications often have heavy dependencies that aren't needed until a specific feature is accessed. Analytics services, payment processors, charting libraries, report generators—these can be megabytes of code that most users never need. Lazy-loading these dependencies reduces your initial bundle and startup time.
How injectAsync works
Traditional approach (loaded at startup):
import { Component } from '@angular/core';
import { ReportService } from './report.service';
@Component({
selector: 'app-report',
template: `<button (click)="generate()">Generate Report</button>`
})
export class ReportComponent {
constructor(private reportService: ReportService) {}
generate() {
this.reportService.createPDF();
}
}
If ReportService is 2MB and includes PDF generation libraries, that 2MB is bundled with your main app even if the user never opens the Reports page.
Lazy approach with injectAsync:
import { Component } from '@angular/core';
import { injectAsync } from '@angular/core';
@Component({
selector: 'app-report',
template: `<button (click)="generate()">Generate Report</button>`
})
export class ReportComponent {
private getReportService = injectAsync(
() => import('./report.service').then(m => m.ReportService)
);
async generate() {
const reportService = await this.getReportService();
reportService.createPDF();
}
}
The ReportService and all its dependencies are now bundled in a separate chunk. When the component loads, it doesn't load ReportService. Only when the user clicks "Generate Report" does the service load (and the browser fetches the chunk if not already cached).
Optional Prefetching with onIdle
You can combine this with the onIdle prefetching hint to load the service in the background when the user isn't interacting:
injectAsync(
() => import('./report.service').then(m => m.ReportService),
{ prefetch: 'onIdle' } // Load during idle time, not at startup
)
This is the sweet spot: the service is ready when the user clicks, but you don't block the initial page load.
Real-World Impact
An analytics dashboard might have 10 different report types. Traditionally, all report generation code loads at startup (5MB). With injectAsync, only the active report's code loads (~500KB). The other 9 are available on-demand. A user running 5 different reports in a session only loads ~2.5MB total instead of 5MB upfront.
Feature 5: stable Resource API (Async State Management)
The Resource API and related async reactivity APIs are stable, ready for production use, providing a more ergonomic approach to loading and managing async state.
The Resource API replaces Async Patterns
Before Angular 22, handling async data required either RxJS subscriptions or manual state management:
interface State {
data: any | null;
loading: boolean;
error: any | null;
}
@Component({
selector: 'app-users',
template: `
<div *ngIf="state.loading">Loading...</div>
<div *ngIf="state.error">{{ state.error }}</div>
<ul *ngIf="state.data">
<li *ngFor="let user of state.data">{{ user.name }}</li>
</ul>
`
})
export class UsersComponent {
state = signal<State>({
data: null,
loading: true,
error: null
});
constructor(private http: HttpClient) {
this.http.get('/api/users').subscribe(
data => this.state.update(s => ({ ...s, data, loading: false })),
error => this.state.update(s => ({ ...s, error, loading: false }))
);
}
}
The Resource API approach:
import { resource } from '@angular/core';
@Component({
selector: 'app-users',
template: `
<div *ngIf="users.isLoading()">Loading...</div>
<div *ngIf="users.error()">{{ users.error() }}</div>
<ul *ngIf="users.data()">
<li *ngFor="let user of users.data()">{{ user.name }}</li>
</ul>
`
})
export class UsersComponent {
users = resource({
loader: () => this.http.get('/api/users')
});
constructor(private http: HttpClient) {}
}
The resource function automatically manages loading state, errors, and retry logic. No manual state objects. No subscription management.
Variants: httpResource
For HTTP requests specifically, use httpResource which bakes in common patterns:
import { httpResource } from '@angular/core';
users = httpResource({
request: () => ({ url: '/api/users' }),
method: 'GET'
});
// Refetch when userId changes
filteredUsers = httpResource({
request: () => ({ url: `/api/users/${this.userId()}` }),
method: 'GET'
});
The Resource API is production-ready now. If you're currently juggling RxJS subscriptions and manual error handling, this is a legitimate upgrade reason.
Feature 6: WebMCP Integration (AI Agents)
With WebMCP, your application and your forms can become tools that AI agents running in the browser can call directly, treating your application and forms as typed capabilities that both people and agents can use.
This is forward-looking infrastructure for the emerging era of AI-powered applications. Instead of building a separate API for AI agents, your Angular forms and services become first-class tools that agents can invoke.
What this means practically
Imagine an AI assistant running in your browser that can fill out your forms, submit requests, and read results. WebMCP provides the contract that makes this possible:
// Your form becomes a typed capability
const userForm = new FormGroup({
email: new FormControl(''),
name: new FormControl('')
});
// An AI agent can inspect the form's schema
// Fill fields based on its reasoning
// Submit and handle the response
// All without hardcoding UI element selectors
This isn't fully detailed yet (it's still integrating), but it signals Angular's direction: applications built with modern Angular will be naturally compatible with AI automation.
Feature 7: Enhanced Router and Template Syntax
The router now inherits route parameters from all parent routes by default, and new route cleanup features improve memory management by giving developers explicit control over route-level dependencies and cached resources.
Route Parameter Inheritance
Before, accessing parent route parameters required manual steps:
constructor(private route: ActivatedRoute) {
this.route.parent?.params.subscribe(params => {
console.log(params);
});
}
Now it's automatic. The ActivatedRoute automatically exposes all parent parameters, reducing boilerplate.
Route cleanup and memory management
Large SPAs with complex routing can leak memory if route-level services and resources aren't cleaned up properly. Angular 22 provides explicit control:
const routeInjector = injector.get(EnvironmentInjector);
// Explicit control over when route-level dependencies are destroyed
// Prevents memory leaks in long-lived applications
This is particularly important for dashboards and admin panels that stay open for hours.
Template syntax additions
Angular 22 adds quality-of-life improvements for templates, including comments within element attribute declarations to document complex templates.
<input
type="email"
[formControl]="emailControl"
<!-- Validates against RFC 5322 standard, allows most real-world addresses -->
(change)="onEmailChange($event)"
/>
This small addition improves template maintainability significantly in complex components.
Feature 8: @boundary (Developer Preview) - Error Boundaries
By wrapping critical or unpredictable code blocks in the new @boundary syntax, an isolated component failure will no longer take down the entire page; errors are caught and developers can specify fallback content.
This is error isolation for components. If a third-party component or complex sub-tree fails, you can catch it and show fallback UI:
@Component({
selector: 'app-dashboard',
template: `
<app-header></app-header>
@boundary() {
<app-analytics-widget /> <!-- If this fails, show fallback -->
} @fallback() {
<div>Analytics currently unavailable</div>
}
@boundary() {
<app-reporting-module />
} @fallback() {
<div>Reports are loading...</div>
}
`
})
export class DashboardComponent {}
This feature is a developer preview (coming Q3 2026), but it's a significant UX improvement for enterprise applications where third-party widgets and integrations are common.
Security improvements (serious business)
Angular 22 is a serious security release, with platform-server protections against server-side request forgery (SSRF) and path hijacking, rejecting suspicious and protocol-relative URLs, and closing SSRF bypasses through backslash URLs in HttpClient.
The framework now sanitizes dynamic href and xlink:href bindings on SVG elements, sanitizes meta selectors, sanitizes placeholder values, and normalizes namespaced tag names.
Most of these apply automatically. But if you're running server-side rendering (Angular Universal), review the HTTP transfer cache behavior:
The HTTP transfer cache now skips cookie-bearing and withCredentials requests, so authenticated, user-specific responses no longer leak into transferred server-side state.
If you cache credentialed responses during Angular Universal, test carefully after upgrading.
TypeScript 6 Support and Angular Aria
Angular 22 fully supports TypeScript 6, allowing developers to use the latest language features and tooling.
Angular Aria is promoted to production-ready status in Angular 22, providing first-class accessibility APIs for building WCAG-compliant interfaces without third-party dependencies.
Incremental hydration is enabled by default, improving time-to-interactive for SSR applications.
Upgrade path and breaking changes
Upgrading to Angular 22 is generally straightforward, especially for applications already running Angular 21; the Angular CLI handles most migration tasks automatically.
The major breaking changes:
- OnPush is default - Angular 21 code gets
ChangeDetectionStrategy.Eager added automatically - Webpack deprecation -
@angular-devkit/build-angular builders and @ngtools/webpack are deprecated in favor of esbuild - HTTP transfer cache skips credentialed requests - Verify if you cache authenticated data during SSR
Run the migration:
ng update @angular/core @angular/cli
The CLI handles most changes. Then review the migration guide at angular.dev/guide/upgrade for the few items requiring manual attention.
Common pitfalls and how to avoid them
1: assuming all forms should migrate immediately
Signal Forms are stable, but your existing Reactive Forms work fine. Migrate incrementally. Pick new features first, then refactor high-value forms. Don't break what's working.
2: not testing OnPush behavior
If you have manual DOM mutations or rely on side effects triggering change detection, OnPush breaks these patterns. Test thoroughly. The automatic migration buys you time to refactor.
3: over-using injectAsync
Every injectAsync call creates an async dependency chain. Too many async dependencies make your component complex. Use injectAsync for genuinely heavy, occasionally-used dependencies. Not for everything.
4: forgetting about route cleanup
If you have long-lived route injectors in complex routing hierarchies, explicitly clean up route-level state. The new route cleanup APIs exist because leaks are real.
5: misunderstanding WebMCP scope
WebMCP is an infrastructure for future AI automation, not something you need to actively use right now. It won't affect your existing code. Ignore it for now, but know it's coming.
Performance considerations
OnPush Default: Expect 20-40% reduction in change detection cycles for typical applications. More significant in dashboards and data-heavy interfaces.
Signal Forms: Slightly smaller bundle size than Reactive Forms (no FormGroup/FormControl classes needed). More direct change propagation means less overhead.
injectAsync with Prefetching: Reduces initial bundle by 5-15% depending on feature usage. Most benefit on slower devices and networks.
Resource API: Cleaner than RxJS subscriptions, but similar runtime performance. The win is reduced boilerplate and fewer subscription leaks.
Incremental Hydration: Faster time-to-interactive for SSR apps, sometimes 30-50% improvement depending on hydration strategy.
F.A.Q.
Q: Do I need to rewrite all my forms to Signal Forms?
No. Reactive Forms still work. Signal Forms are the recommended path for new features, but migration is gradual. Focus on new forms first.
Q: What if my app breaks with OnPush default?
ng update adds ChangeDetectionStrategy.Eager to existing components automatically. You have a clear migration path. Remove the Eager annotations one by one as you refactor.
Q: Is WebMCP mandatory?
Not at all. It's infrastructure for future AI integration. Your app works without it. Adopt it when AI automation is relevant to your product.
Q: Should I upgrade from Angular 21 immediately?
If you're already on Angular 21, the upgrade is low-risk. Most migration is automated. If you're on Angular 19 or earlier, plan a more careful upgrade path. Each major version introduces changes that compound.
Q: How does @Service differ from @Injectable?
@Service() is the new default for services. @Injectable({ providedIn: 'root' }) still works but is verbose for the common case. Both work, but @Service() is the modern approach.
Conclusion: Angular's Inflection Point
Angular 22 isn't a complete reinvention of the framework, but it does mark an important shift; many features introduced as previews, experiments, or optional enhancements in Angular 21 have now become stable, production-ready, and, in some cases, the default behavior.
This is the release where Angular's signal-first vision stops being theoretical and becomes your daily reality. If you've been waiting for stability before committing to Signals, OnPush, and reactive forms, the wait is over.
The upgrade is straightforward, the features are production-ready, and the performance improvements are real. For teams building modern web applications, Angular 22 removes the last reasons to hesitate.
Start with your next feature, not a full rewrite. Adopt Signal Forms for new forms. Trust that OnPush is safe (the migration handles it). Let injectAsync reduce your bundle when it makes sense. The framework supports both old and new patterns during transition.
Angular 22 isn't just an update. It's a confirmation that years of design work have paid off. Build on it confidently.
Further Reading