Where Versions Stand
| Version | Status | Note |
|---|---|---|
| 22 | Active | Signal Forms stable, OnPush default for new components |
| 21 | LTS to May 2027 | Zoneless default for new apps |
| 20 | LTS to Nov 2026 | Zoneless stable in v20.2 |
| 19 | EOL | Upgrade immediately |
ng update moves one major at a time. v22 requires TypeScript 6 and drops Node 20.
Writing Components in v22
Work through these in order when modernizing a feature: dependencies, state, templates, data, then bootstrapping.
Dependency injection with inject()
TypeScriptConstructor injection was the default for years and still works on legacy code.
inject() is shorter, works in functions and tests, and is the team standard for new components.
@Component({ ... })
export class UserListComponent {
private userService = inject(UserService);
private route = inject(ActivatedRoute);
}Component state with signals
TypeScriptWe stored state in class fields or Observables and subscribed in ngOnInit.
Signals give synchronous reads in templates. computed() derives values without manual subscription cleanup.
export class ProfileComponent {
private userService = inject(UserService);
user = this.userService.user;
displayName = computed(() => this.user()?.name ?? 'Guest');
}Template control flow
HTML*ngIf and *ngFor were the standard for a decade. They still work in existing templates.
Angular recommends @if, @for, and @switch for new development. They are built into the compiler and easier to read.
@if (user(); as u) {
<p>{{ u.name }}</p>
} @else {
<p>Loading...</p>
}
@for (item of items(); track item.id) {
<li>{{ item.name }}</li>
}HTTP with httpResource()
TypeScriptWe manually subscribed in ngOnInit and tracked loading and error flags ourselves.
httpResource() is stable in v22. It refetches when signal inputs change and cancels stale requests. Keep HttpClient in services for mutations.
userId = signal('42');
users = httpResource(() => ({
url: '/api/users',
params: { managerId: this.userId() },
}));
// template: users.value(), users.isLoading(), users.error()Signal Forms
TypeScriptReactive Forms with FormGroup and FormControl were the only typed forms option.
Signal Forms graduated to stable in v22 (Angular v22 announcement). Use them for new simple forms. Keep Reactive Forms for large, dynamic, or highly conditional forms.
profileForm = form(signal({ email: '', name: '' }), (f) => {
required(f.email, { message: 'Email required' });
email(f.email);
});
onSubmit() {
if (this.profileForm().invalid()) return;
this.api.save(this.profileForm().value());
}OnPush change detection
TypeScriptChangeDetectionStrategy.Default (now renamed Eager) was the implicit default for every component.
v22 sets OnPush as the default for new components without an explicit strategy (Angular RFC #66779). ng update adds Eager to existing components to preserve their behavior.
// New components in v22: OnPush is implicit
// Legacy components after ng update:
@Component({
changeDetection: ChangeDetectionStrategy.Eager,
})
export class LegacyDashboardComponent { }@Service() decorator
TypeScript@Injectable({ providedIn: 'root' }) was verbose boilerplate on every root-scoped service.
@Service() is stable in v22 (angular/angular, Apr 2026). It is shorthand for root-provided singletons and requires inject() for dependencies. @Injectable() remains for advanced DI scopes.
@Service()
export class AuthService {
private http = inject(HttpClient);
}Zoneless change detection
TypeScriptEvery app shipped Zone.js in polyfills. It patched async APIs to trigger change detection globally.
Zoneless removes the Zone.js bundle (~33KB, Angular v21 blog). Pilot on one module before removing zone.js globally.
bootstrapApplication(AppComponent, {
providers: [
provideZonelessChangeDetection(),
provideHttpClient(),
],
});Signals, RxJS, and Decision Matrices
Signals are not a replacement for RxJS. Signals own component state and template bindings. RxJS owns time-based async orchestration.
| Use case | Signals | RxJS |
|---|---|---|
| Component state and derived values | Yes | |
| Template binding | Yes | |
| Debounce, throttle, retry | Yes | |
| combineLatest, switchMap, mergeMap | Yes | |
| WebSockets and server-sent events | Yes | |
| Bridge between the two | toSignal() / toObservable() |
Debounced search
TypeScriptA Subject plus an Observable pipeline was the classic search-as-you-type pattern.
Signal for the input, RxJS for debounce logic, toSignal() for the template. Do not replace every Observable in the codebase.
query = signal('');
results = toSignal(
toObservable(this.query).pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((q) => this.api.search(q)),
),
{ initialValue: [] },
);Zoneless vs Zone.js
| Scenario | Zoneless | Keep Zone.js |
|---|---|---|
| New greenfield modules | Yes | |
| Third-party lib requires Zone.js | Yes | |
| Gradual migration | Hybrid: zoneless provider + Zone.js polyfill | |
| Maximum bundle size reduction | Yes |
Migration Order
- Get current on v21 or v22. One major per sprint.
- Zoneless pilot on a low-traffic module. Keep Zone.js if a library still needs it.
- OnPush on hot paths. Let
ng updatesetEageron legacy components first. - Signal stack for new work:
httpResource, Signal Forms, NgRx SignalStore for shared state. - Polish templates and tests:
@if/@for,NgOptimizedImage, Vitest withfixture.whenStable().
Get to a supported version first. Then adopt v22 patterns on every file you touch. Zoneless + OnPush + signals together deliver more than any single change in isolation.