Angular 22 is finally out, and this release brings a set of interesting updates, all tied to the Signal ecosystem and webapp optimization. In this article we cover the most important things to know.
Signal Forms is now stable
Let’s start with one of the most relevant updates: Signal Forms are stable and production-ready. Experimental in Angular 21, it can now be used without reservations.
For new forms we can drop FormGroup, FormControl and valueChanges: Signal Forms combines Reactive Forms, strong typing and Signal reactivity into a single declarative, composable API.
The core is the form function, which takes a Signal with the form data and an optional schema of validation rules:
import { signal } from '@angular/core';
import { form, required, minLength } from '@angular/forms/signals';
@Component({ imports: [FormField] /* ... */ })
export class JediForm {
protected readonly jedi = signal({ name: '', rank: '' });
protected readonly myForm = form(this.jedi, (path) => {
required(path.name, { message: 'This field is mandatory' });
minLength(path.name, 3);
});
}The result is a FieldTree: a structure of nested Signals where each field exposes its own state (value, dirty, invalid, errors). In the template we connect it with the FormField directive:
<input [formField]="myForm.name" id="jedi-name" />
@if (myForm.name().invalid() && myForm.name().touched()) {
@for (error of myForm.name().errors(); track error.kind) {
<span class="error">{{ error.message }}</span>
}
}Around this we have a complete stack: the Submission API to handle form submission, dynamic schemas with validateStandardSchema (compatible with Zod and Valibot), conditional CSS classes and interop with existing Reactive Forms. The updated official guide is at angular.dev/guide/forms.
As an Angular developer, I find the Zod schema-based validation particularly interesting, especially in enterprise applications where validation rules can change based on multiple variables.
The Resource API is now stable
resource, httpResource and rxResource are out of experimental state. The Resource API was the missing piece of the Signal ecosystem, letting us derive data reactively and asynchronously – typically HTTP GET calls that re-run when a Signal changes. A typical use case is a list that needs to reload when user-populated filters change.
The most convenient entry point is httpResource: it takes a lambda that returns the request. If a Signal used inside it changes, the request fires again automatically.
import { httpResource } from '@angular/common/http';
import { signal } from '@angular/core';
export class ShipSearch {
readonly faction = signal('rebel');
readonly ships = httpResource<Ship[]>(
() => ({
url: 'https://api.example.io/ships',
params: { faction: this.faction() },
}),
{ defaultValue: [] },
);
}The resource manages state through Signals: value holds the data, error the eventual error, isLoading the loading state (there’s also a more detailed status). The defaultValue prevents us from ending up with undefined on startup.
ā¹ļø Race conditions are handled automatically: if multiple requests come in close succession, only the last one is used and the previous ones are cancelled, just like switchMap in RxJS.
New @Service decorator
A new @Service decorator has been introduced, replacing the most widely used pattern of all: @Injectable({ providedIn: 'root' }). It better expresses the intent of registering a globally scoped service.
import { Service } from '@angular/core';
@Service()
export class FleetStore { /* ... */ }By default the service is provided in the root scope. @Injectable is still available for cases that require deeper configuration or constructor injection.
OnPush by default
From Angular 22, all new components use OnPush automatically as the change detection strategy. This aligns with the update dynamics introduced with Signals and the move away from zone.js.
// From Angular 22 this component uses OnPush without declaring it
@Component({
selector: 'app-dashboard',
template: `...`,
})
export class Dashboard {}The old Default has been renamed to Eager, a name that better describes what it does: it checks the entire component tree for changes.
@Component({
selector: 'app-legacy',
changeDetection: ChangeDetectionStrategy.Eager,
template: `...`,
})
export class Legacy {}The update involves no breaking changes: for components without an explicit strategy, ng update automatically adds Eager to preserve the previous behavior. The migration to OnPush can be done at your own pace, one component at a time.
injectAsync: lazy loading for services
This is another update I’m genuinely excited about. Angular already supported lazy loading for components and routes, but not for services. With injectAsync, service lazy loading becomes an out-of-the-box framework feature: the bundle is downloaded only on the first invocation, making the component lighter and faster to load.
import { injectAsync } from '@angular/core';
export class Report {
private exporter = injectAsync(() => import('./report-exporter'));
async export() {
const exporter = await this.exporter();
exporter.export();
}
}ā ļø Passing the import directly only works if the module exposes a default export. Otherwise you need to specify the explicit export, for example injectAsync(() => import('./report-exporter').then(m => m.ReportExporter)).
To avoid the delay on the first call, we can prefetch with onIdle, which loads the service in the background while the browser is idle:
import { injectAsync, onIdle } from '@angular/core';
private exporter = injectAsync(
() => import('./report-exporter'),
{ prefetch: onIdle },
);For this to work, the service must be auto-provided (with @Service() or @Injectable({ providedIn: 'root' })).
We can use this feature to lazily load all services that a component needs but that don’t play a primary role in its initial load, maybe tied to functionality the user might use later.
FetchBackend as default in HttpClient
HttpClient now uses the Fetch API by default instead of XMLHttpRequest, so the old withFetch() is deprecated and can be removed. Fetch is more modern, Promise-based and better suited for SSR scenarios.
One limitation to keep in mind: fetch doesn’t support upload progress events. For this reason, the generic reportProgress option is replaced by two dedicated variants:
// Download progress (works with Fetch)
http.get('/file', { reportDownloadProgress: true, observe: 'events' });
// Upload progress (requires withXhr())
http.post('/upload', file, { reportUploadProgress: true, observe: 'events' });If we need upload progress, we can switch back to XHR with provideHttpClient(withXhr()) (ng update adds this where needed).
Angular Aria is now stable
Angular Aria is production-ready: 12 headless accessibility patterns (accordion, listbox, menu, tree and others) are ready to use. The approach is to leave styles and business logic to us, while Angular Aria takes care of accessibility.
In this version the APIs have been stabilized, test harnesses have been added, and there is full integration with Signal Forms. The pattern overview is at angular.dev.
Angular MCP: new tools for the dev server
On the AI front, the CLI’s MCP server gains three new tools for interacting with the development server:
devserver.startto start the serverdevserver.stopto stop itdevserver.wait_for_buildto wait for the build and read its output
The most interesting one is devserver.wait_for_build: it lets an AI agent compile the project, read the logs and decide the next steps based on the errors found, enabling self-correction loops. These tools become stable in this release, along with those for tests and end-to-end.
Template: syntax improvements
Templates become more expressive, letting us delegate less to the component. The main additions:
//and/* */comments inside component templates, useful for documenting properties and bindings.- Spread/rest in objects, arrays and function calls, just like in TypeScript.
- Inline arrow functions in event bindings.
- Multiple consecutive
@caseblocks in@switch, to avoid duplication. - Exhaustive check with
@default never: if we add a value to a union without handling it, the compilation fails.
Some examples:
<div
// primary button
class="btn btn-primary"
/* disabled only during loading */
[disabled]="loading()"
></div>
<div [class]="{ ...baseClasses, active: isActive() }"></div>
@switch (orderStatus) {
@case ('pending')
@case ('processing') { <p>In progress</p> }
@case ('shipped') { <p>Shipped</p> }
@default never;
}Incremental hydration by default
provideClientHydration() now enables incremental hydration automatically, with no extra configuration. If we don’t need it, we can disable it explicitly with withNoIncrementalHydration().
Official Angular agent skills
Keeping up with a growing API surface is hard not just for us, but also for AI assistants, whose training data often doesn’t cover the latest patterns and generates code that’s already outdated from the start. To close the gap, the Angular team has released official skills that teach agents the modern patterns of the framework:
angular-developer: best practices and guidelines for writing modern Angular applications, including recent features like Angular Aria and Signal Forms.angular-new-app: designed for those starting from scratch, it guides the assistant through setting up a local Angular environment.
You can find them at github.com/angular/skills and they work in agentic environments. I’ve personally been using these skills with Claude Code and GitHub Copilot for quite some time.
Webpack deprecated
An important build change: in v22, @angular-devkit/build-angular, @ngtools/webpack and Webpack support are deprecated. Their place is taken by the application builder based on esbuild and Rollup (the @angular/build package), which has long been the default for new projects.
On a different front, the team is exploring TSGo, the TypeScript compiler rewritten in Go. It’s not a bundler or a Webpack replacement, but it aims to speed up type checking and compilation. It’s still early-stage work and not directly tied to the application builder, but in the future it will allow us to dramatically cut Angular’s compile and build times.
debounced() (experimental)
Signals, by their nature, have no concept of time: no delay, no throttling. Resources do, and that’s where the new debounced function comes in (still experimental in v22, so the API may change): a signal-first debounce that creates a Resource<T> with the delayed value, loading state during the wait and built-in error handling.
import { debounced } from '@angular/core';
const filter = signal('');
const debouncedFilter = debounced(filter, 300); // 300ms
effect(() => console.log(debouncedFilter.value()));WebMCP (experimental)
Among the experimental features, WebMCP stands out. It lets an Angular application expose structured tools to AI agents directly in the browser, without relying on DOM automation. We can define tools at the app, route or component level, and automatic tool generation from dynamic Signal Forms is also planned.
It’s an experimental feature: at the moment it requires Chrome Beta with the relevant flag enabled. It’s documented at angular.dev.
A look ahead: @boundary
Not available yet, but the team has previewed @boundary: error boundaries directly in templates. We wrap an “at-risk” block and, if a component throws, instead of breaking the whole page we show a fallback.
@boundary {
<app-promo-widget />
}
@error (let err) {
<app-default-promo />
}The timeline is not yet confirmed (probably Angular 22.1 or 23) and the syntax above is indicative and may change. We’ll revisit it when the time comes, but I wanted to include it because it opens up some really interesting implementation scenarios.
Conclusions
Angular 22 is a maturity release in the Signal era: with Signal Forms, Resource API and Angular Aria finally stable, we have fundamental building blocks ready for production, while @Service, injectAsync, debounced, the template improvements and the AI-related work complete the picture.
Is it worth upgrading right away? And what do we do if we already have complex applications that don’t use Signals? From my experience, the advice is to evaluate case by case: the upgrade itself is safe (change detection behavior is preserved automatically), while adopting the new primitives is best done gradually, on new features and refactors. RxJS-based code is not something to throw away, but the direction of the framework is now clearly Signal-oriented. In other cases we’ll definitely need to wait for updates from other libraries, for example NX (though they’re usually very fast and typically have a version compatible with the latest Angular release within a few days).
Thanks for reading, see you next time! š