Web Development · Frameworks
Angular signals and zoneless migration guide for v21 and v22
Angular 21 made zoneless the default for new apps and v22 made OnPush the default. What that means for signals, migration steps and which version to target.
Anurag Verma
11 min read · Updated Oct 19, 2026
Sponsored
This guide started life as an Angular 21 explainer. Angular 22 shipped in June 2026, so it now covers both: what signals and zoneless change detection look like today, which APIs are stable in which version, and the order of work for moving an older zone.js app over.
Signals plus zoneless change detection is now the default shape of a new Angular app.
What changed in Angular 21 and 22
Angular 21 made zoneless change detection the default for new apps, and Angular 22, released June 3, 2026, made OnPush the default for every component that doesn’t pick a strategy. Signals have been stable since v17, and signal inputs, outputs and queries since v19. Migrating an existing app means moving state into signals first, then removing zone.js.
Here is where each supported version stands in October 2026, from Angular’s release page and CHANGELOG:
| Version | Released | Status (Oct 2026) | What it changed for change detection |
|---|---|---|---|
| v20 | 2025-05-28 | LTS, ends 2026-11-28 | Zoneless promoted to stable in 20.2 (opt in) |
| v21 | 2025-11-19 | LTS, ends 2027-06 | New projects are zoneless by default |
| v22 | 2026-06-03 | Active, current (22.2.3) | OnPush is the default component strategy |
One more thing changed with v22, and it matters for planning. Angular used to ship a major every six months. The release page now says the team aims for one major every 12 months, with 4 to 6 minors in between and roughly 24 months of support per major (12 active, 12 LTS). v22 is in active support until June 2027, so don’t expect v23 much before then. Anything older than v20 is already out of support.
Why zone.js was the problem
Angular’s original change detection relied on zone.js, which monkey-patches the browser’s async APIs (setTimeout, Promise, addEventListener, XMLHttpRequest, fetch and more). When any async operation finished, zone.js told Angular, and Angular checked the component tree.
That was reliable and wasteful. zone.js can’t tell whether an async callback changed any state, so a mousemove handler on a canvas could trigger a check of every binding on the page. The zoneless guide puts it plainly: zone.js “brings a fair amount of overhead, both in payload size and startup time,” and triggers change detection more often than needed. Teams spent years working around it with ChangeDetectionStrategy.OnPush, NgZone.runOutsideAngular() and manual detectChanges() calls.
Signals fix the root cause. Angular knows which views read which signals, so it can schedule work only when something those views depend on actually changes.
Signals: what is stable, and since when
A signal is a value that tells its readers when it changes. The core API has been stable since v17:
import { signal, computed, effect } from '@angular/core';
const count = signal(0);
// Computed values derive from other signals
const doubled = computed(() => count() * 2);
// Effects run when the signals they read change
effect(() => {
console.log(`Count is ${count()}, doubled is ${doubled()}`);
});
count.set(1); // logs "Count is 1, doubled is 2"
count.update(v => v + 1); // logs "Count is 2, doubled is 4"
The rest of the signal family went stable in stages, according to the CHANGELOG. Signal-based input(), output(), model() and the signal query functions (viewChild(), contentChildren() and friends) were marked stable in v19. linkedSignal(), toSignal() and toObservable() followed in v20. The async side (resource(), httpResource(), rxResource()) was introduced as experimental in v19, and the v22 release page lists asynchronous signals as stable.
None of this is new to frontend in general. Vue’s ref() and computed(), SolidJS signals and Svelte 5 runes all work on the same principle, and our signals explainer walks through how the dependency tracking works under the hood. Angular converging on a proven model is a good thing. It also means the skills transfer.
Signal inputs, outputs and queries
Signal inputs replace @Input() setters and most ngOnChanges code, and signal queries replace @ViewChild with its { static } option:
import { Component, ElementRef, input, output, viewChild } from '@angular/core';
@Component({
selector: 'app-search',
template: `
<input #searchInput [value]="query()" (input)="onSearch($event)" />
<p>Results for: {{ query() }}</p>
`
})
export class SearchComponent {
query = input<string>('');
search = output<string>();
searchInput = viewChild<ElementRef>('searchInput');
onSearch(event: Event) {
const value = (event.target as HTMLInputElement).value;
this.search.emit(value);
}
}
Notice there is no standalone: true. Components, directives and pipes have been standalone by default since v19, so the flag is noise in new code. A component lists what it uses in imports:
import { Component, input } from '@angular/core';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-user-card',
imports: [RouterLink],
template: `
<div class="user-card">
<h2>{{ user().name }}</h2>
<a [routerLink]="['/users', user().id]">View profile</a>
</div>
`
})
export class UserCardComponent {
user = input.required<User>();
}
NgModules still work, and large shared libraries sometimes keep them. New code rarely needs one.
Async data with resource()
Before resources, fetching data that depends on an input usually meant RxJS: an observable of the ID, switchMap to the request, then toSignal() or the async pipe. resource() folds that into one call. The params function is reactive, and the loader runs again when the params change. This is the example from the resource guide:
const userId: Signal<string> = getUserId();
const userResource = resource({
params: () => ({id: userId()}),
loader: ({params}) => fetchUser(params),
});
httpResource() does the same on top of HttpClient, so interceptors still apply. RxJS remains fully supported (rxResource() takes an observable loader), but simple fetch-and-display code no longer forces it on you. I think that is the change newer Angular developers will notice most, more than zoneless.
If you wrote resource code against v19 to v21, check the v22 changelog before upgrading. The API kept moving while it was experimental, and v22 added features such as SSR caching for resources and debounced signals.
Signal Forms
Signal Forms live in @angular/forms/signals. They were experimental in v21 and are listed as stable for v22. The model is a plain signal, validation lives in a schema function, and the template binds fields with the FormField directive. From the Signal Forms essentials page:
import { Component, signal } from '@angular/core';
import { form, FormField, required, email } from '@angular/forms/signals';
interface LoginData {
email: string;
password: string;
}
@Component({
selector: 'app-login',
imports: [FormField],
template: `<input type="email" [formField]="loginForm.email" />`
})
export class LoginComponent {
loginModel = signal<LoginData>({ email: '', password: '' });
loginForm = form(this.loginModel, (schemaPath) => {
required(schemaPath.email, { message: 'Email is required' });
email(schemaPath.email, { message: 'Enter a valid email address' });
});
}
Compare that with FormBuilder, FormGroup, FormControl and a Validators array spread across files. Reactive forms aren’t deprecated, and I wouldn’t rewrite a working form just to switch. For new forms on v22, Signal Forms are the better default.
Migrating an existing app to zoneless
ng update handles version bumps and runs the bundled migrations, but it can’t make your state management zoneless for you. Code that mutates a plain object and expects the screen to refresh worked under zone.js and silently stops working without it. The order below follows the zoneless guide:
- Upgrade one major at a time (
ng update @angular/core@21 @angular/cli@21, then 22) and let each version’s migrations run. On v22, review where the migration addedChangeDetectionStrategy.Eager. Each of those components still relies on the old check-everything behavior. - Make components OnPush compatible. Angular only refreshes a view when it gets a notification: a signal read in the template changing, a bound template event,
markForCheck(),ComponentRef.setInput(), or theAsyncPipe(which callsmarkForCheck()for you). State that changes any other way needs to move into a signal. - Watch reactive forms. They don’t schedule change detection on their own, so connect
valueChangestomarkForCheck()or expose the state through signals. - Remove code that waits on
NgZone.onStable,onMicrotaskEmpty,onUnstableorisStable. UseafterNextRender()orafterEveryRender()instead. - Turn it on. Add
provideZonelessChangeDetection()to the bootstrap providers (v20 apps and older upgraded apps need this, and new v21+ apps already have it), and make sure noprovideZoneChangeDetection()call is overriding it. - Remove
zone.jsandzone.js/testingfrom thepolyfillsarrays inangular.jsonfor both build and test targets, thennpm uninstall zone.js. - Fix tests. TestBed is zoneless by default even when zone.js is still loaded. The guide prefers
await fixture.whenStable()overfixture.detectChanges(), andprovideCheckNoChangesConfig({ exhaustive: true })helps catch bindings that changed without a notification.
Step 2 is where the time goes. The CLI can rewrite decorators. It can’t tell you that a websocket handler three services away pushes into an array your template reads.
Other v22 breaking changes to plan for
The 22.0.0 CHANGELOG has a long breaking changes list. The ones most likely to touch an enterprise app:
- TypeScript older than 6.0 is no longer supported.
HttpClientnow uses the fetch backend by default andwithFetch()is deprecated. If you need upload progress events, switch toprovideHttpClient(withXhr()).- The router’s
paramsInheritanceStrategynow defaults to'always', so child routes inherit params from all parents unless you set'emptyOnly'. ComponentFactoryResolver,ComponentFactory,createNgModuleRefandprovideRoutes()are gone, and so is the Hammer.js integration.- Incremental hydration is now the default behavior for SSR apps.
What about performance numbers?
An earlier version of this post had an Angular 16 vs 21 table of bundle sizes, change detection counts and onboarding days. Those figures weren’t backed by a published benchmark, so they’re gone. Angular’s own docs describe the zone.js cost qualitatively and don’t publish a headline “X% faster” number for zoneless, and I’d be suspicious of anyone who quotes one without their test app attached.
What you can say honestly: without zone.js there is one less polyfill to ship and no change detection triggered by async events that don’t touch state. How much that buys you depends on how your app is written. An app already using OnPush everywhere will see less difference than one that relied on default change detection with lots of timers and event listeners. Profile before and after with Angular DevTools and the browser’s performance panel on your own heaviest screen. My guess is the bigger long term win is predictability: you can read a component and know what will cause it to update.
Who should care
Angular’s pitch has always been structure at scale. It ships its own router, forms, HTTP client and test tooling, so a developer moving between squads in a large organization finds the same patterns everywhere. React’s flexibility suits a team of five and becomes a coordination cost at fifty, when every squad has picked a different state library. Angular is written in TypeScript and type-checks templates, which matters in regulated codebases where catching errors at compile time is cheaper than catching them in an audit.
The one-major-a-year cadence from v22 also helps here. Enterprise teams that struggled to keep up with two majors a year now get a slower, more plannable upgrade rhythm.
It is still the wrong pick in some cases:
- Small apps, MVPs and prototypes you expect to throw away. The structure is overhead when the requirements change weekly.
- Content-heavy sites such as blogs, docs and marketing pages. Astro, Nuxt or Next.js fit better, even with Angular’s improved SSR.
- Teams with deep React expertise and a healthy React 19 codebase. The gap is too small to justify a rewrite. The same goes for teams looking at Vue’s Vapor Mode: the frameworks are converging, so team fit should decide.
For a wider side by side, see React vs Angular vs Vue in 2026.
Which version to target
Start new projects on v22. Signal Forms and the resource APIs are stable there, and you get zoneless plus OnPush by default without opting in to anything.
For an existing app on v20 or older, get to v21 first and do the zoneless work there, since v21 LTS gives you until June 2027. Then move to v22 and deal with the OnPush default and the TypeScript 6 requirement as a separate step. Doing both at once makes it hard to tell which change broke a screen. v20 apps have the tightest deadline, with support ending in late November 2026.
Planning an Angular upgrade or a zoneless migration and short on hands? Read how to hire an Angular developer, or talk to CODERCOPS about the project.
Frequently asked questions
- Is Angular 21 zoneless by default?
- For new projects, yes. A v21 project created with ng new no longer includes zone.js, and the angular.dev zoneless guide says no setup is needed from v21 on. Apps upgraded from older versions keep zone-based change detection until you opt in with provideZonelessChangeDetection() and remove zone.js from the polyfills.
- What is the latest Angular version in October 2026?
- Angular 22. It was released on June 3, 2026, and the latest stable patch as of mid October 2026 is 22.2.3, with 22.3 in prerelease. Angular 21 is in long-term support until June 2027, and Angular 20 drops out of support at the end of November 2026.
- What changed in Angular 22 for change detection?
- Components that don't declare a changeDetection strategy are now OnPush by default. To keep the old check-everything behavior on a component, set changeDetection to ChangeDetectionStrategy.Eager. The v22 release includes a migration that adds Eager where it applies, so treat that diff as a to-do list rather than permanent code.
- Are Angular Signal Forms stable?
- Signal Forms arrived as an experimental API in v21 under @angular/forms/signals. Angular's v22 release page lists Signal Forms, asynchronous signals (the resource APIs) and Angular Aria as stable, so v22 is the version to target if you want to build new forms on them.
- Do Angular signals work like Vue's ref() or Svelte 5 runes?
- Architecturally, yes. All three track which consumers read a piece of state, so a change only updates the views that depend on it instead of checking a whole component tree. Angular arrived at this later than Vue and Svelte, but the mechanism is the same.
- Should we migrate an existing React app to Angular?
- Usually no. If your team knows React well and the app is in good shape, the switch costs more than it returns for most workloads. Current Angular is worth a look for a new enterprise build with a large team, not as a rewrite target for something that already works.
Sources
- Angular versioning and releases: release dates, support windows and the v22 cadence change (angular.dev)
- Angular v22 release page: Signal Forms, asynchronous signals and Angular Aria stable (angular.dev)
- Angular CHANGELOG: 22.0.0 breaking changes, OnPush default, stability promotions by version (GitHub)
- Angular releases on GitHub (latest 22.2.3, October 14, 2026)
- Angular without ZoneJS (zoneless) guide (angular.dev)
- Async reactivity with resources guide (angular.dev)
- Signal Forms essentials (angular.dev)
Sponsored
More from this category
More from Web Development
Sponsored
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored