Skip to main content
Back to Blog

Angular 22 Best Practices: Signals, Zoneless, and Enterprise Architecture in 2026

Angular 22 migration guide: stable Signal Forms, zoneless change detection, OnPush defaults, and when to keep RxJS for enterprise apps.

·angular, frontend, typescript

Where Versions Stand

VersionStatusNote
22ActiveSignal Forms stable, OnPush default for new components
21LTS to May 2027Zoneless default for new apps
20LTS to Nov 2026Zoneless stable in v20.2
19EOLUpgrade 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()

TypeScript

Constructor 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

TypeScript

We 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()

TypeScript

We 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

TypeScript

Reactive 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

TypeScript

ChangeDetectionStrategy.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

TypeScript

Every 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.

Signal
RxJS Operators
toSignal
Template
Bridging signals and RxJSuse each where it excels
Use caseSignalsRxJS
Component state and derived valuesYes
Template bindingYes
Debounce, throttle, retryYes
combineLatest, switchMap, mergeMapYes
WebSockets and server-sent eventsYes
Bridge between the twotoSignal() / toObservable()

Debounced search

TypeScript

A 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

ScenarioZonelessKeep Zone.js
New greenfield modulesYes
Third-party lib requires Zone.jsYes
Gradual migrationHybrid: zoneless provider + Zone.js polyfill
Maximum bundle size reductionYes

Migration Order

Get Current
Zoneless Pilot
OnPush
Signal Stack
Polish
One quarter per stage
  1. Get current on v21 or v22. One major per sprint.
  2. Zoneless pilot on a low-traffic module. Keep Zone.js if a library still needs it.
  3. OnPush on hot paths. Let ng update set Eager on legacy components first.
  4. Signal stack for new work: httpResource, Signal Forms, NgRx SignalStore for shared state.
  5. Polish templates and tests: @if/@for, NgOptimizedImage, Vitest with fixture.whenStable().
The rule

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.