DEV Community

Cover image for Angular 22: The End of Boilerplate and the Consolidation of the Reactive Era
Ghabryel Henrique Ferreira e Almeida
Ghabryel Henrique Ferreira e Almeida

Posted on • Originally published at zyvop.com

Angular 22: The End of Boilerplate and the Consolidation of the Reactive Era

If you have been following the evolution of Google's framework over the last few years, you know it has been undergoing a silent reconstruction — piece by piece. With the release of Angular 22 on June 3, 2026, this reconstruction is no longer a promise and has become the standard. We are not looking at another batch of experimental features: we are looking at the consolidation of an entirely rethought ecosystem.

For those who live and breathe enterprise applications, Clean Architecture, and Microfrontend ecosystems, this is the version that finally delivers what has been promised since Angular 16: an end-to-end reactive framework, zone-less by nature, and with much less ceremony along the way.

The experiments are over. Below is what has actually changed — and what you need to do before running ng update.

📖 If you are just starting out: several technical terms in this article (change detection, Signals, SSR, dependency injection, microfrontends...) are explained in a glossary at the end. Read the article from end to end and use the glossary as a reference whenever you have a doubt.

What Arrived in Angular 22

  • OnPush is the new default change detection (the old Default became Eager and is deprecated).

  • Stable Resource API: resource, rxResource, and httpResource are ready for production.

  • Stable Signal Forms: featuring the Submission API, dynamic schemas (Zod/Valibot), and interop with Reactive Forms.

  • New @Service() decorator: shortening @Injectable({ providedIn: 'root' }).

  • injectAsync: for lazy dependency injection, with prefetch via onIdle.

  • debounced: for native debounce in Signals/Resources.

  • Incremental Hydration: enabled by default.

  • HttpClient: uses FetchBackend by default (withFetch() is deprecated).

  • Important Router and bootstrap improvements designed for Microfrontends.


1. OnPush as the New Default Change Detection

The moment the community has always asked for has arrived: ChangeDetectionStrategy.OnPush is now the default behavior for any new component. This decision makes perfect sense in a signals-first world — those who use Signals already receive surgical notifications about what changed, and OnPush takes full advantage of this, checking only the truly affected components instead of scanning the entire tree.

The old Default (which checked the whole tree) was renamed to Eager and is deprecated. If you still need the old behavior in a component, declare it explicitly:

import { ChangeDetectionStrategy, Component } from '@angular/core';

@Component({
  selector: 'app-legacy',
  changeDetection: ChangeDetectionStrategy.Eager, // replaces the old 'Default'
  template: `...`,
})
export class LegacyComponent {}

Enter fullscreen mode Exit fullscreen mode

Critical detail for the migration: during ng update, if Angular doesn't find an explicit strategy, it automatically applies Eager so nothing breaks. That is, you don't get performance "for free" — you need to migrate component by component to reap the benefits of OnPush.

2. Stable Resource API and httpResource

The Resource API was the missing piece in the Signals puzzle: deriving asynchronous data reactively, usually by triggering HTTP requests when a Signal changes. Now resource, rxResource, and httpResource are stable and cleared for production.

The most comfortable entry point is httpResource. It takes a reactive lambda that returns the request: if a Signal used inside it changes, the request is automatically re-fired.

import { httpResource } from '@angular/common/http';
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { Flight } from './flight';

@Component({
  selector: 'app-flight-search',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @if (flightsResource.isLoading()) {
      <div>Loading…</div>
    } @else if (flightsResource.error()) {
      <div>Error: {{ flightsResource.error() }}</div>
    } @else {
      @for (flight of flightsResource.value(); track flight.id) {
        <app-flight-card [item]="flight" />
      }
    }
  `,
})
export class FlightSearch {
  protected readonly filter = signal({ from: 'São Paulo', to: 'Uberlândia' });
  protected readonly flightsResource = httpResource<Flight[]>(
    () => ({
      url: 'https://api.example.io/flight',
      params: { from: this.filter().from, to: this.filter().to },
    }),
    { defaultValue: [] }, // prevents the component from dealing with `undefined` on initialization
  );

  protected reload(): void {
    this.flightsResource.reload();
  }
}

Enter fullscreen mode Exit fullscreen mode

The resource manages its own state via Signals: value, error, isLoading, and a more detailed status (idle, loading, reloading, error, resolved, local). And the best part: race conditions are handled automatically — if several requests arrive in sequence, only the result of the most recent one is used, exactly like switchMap would do in RxJS, but without you writing a single line of pipe.

Want to skip the request under certain conditions? Just return undefined in the lambda.

3. Signal Forms Ready for Production

The eternal divide between Reactive Forms and Template-Driven is over. Signal Forms have left experimental status and are the recommended approach for forms: declarative, strongly typed, and reactive via Signals.

The heart of the API is the form function, which receives a Signal with the data and a validation schema:

import { form, minLength, required } from '@angular/forms/signals';

protected readonly flightForm = form(this.flight, (path) => {
  required(path.from);
  required(path.to);
  required(path.date);
  minLength(path.from, 3);
});

Enter fullscreen mode Exit fullscreen mode

The result is a FieldTree: a nested structure of Signals where each field exposes value, dirty, invalid, and errors. In the template, you use the FormField directive:

<input [formField]="flightForm.from" id="flight-from" />
<div>{{ flightForm.from().errors() | json }}</div>

Enter fullscreen mode Exit fullscreen mode

And it doesn't stop there. Angular 22 (adding to 21.1 and 21.2) brought a surprisingly complete forms stack:

  • Submission API (FormRoot + submit): all submission logic inside the form itself, including mapping validation errors from the server back into the form state.

  • Dynamic schemas with validateStandardSchema, compatible with Zod and Valibot — and which re-evaluate when a Signal changes.

  • Conditional CSS classes (ng-valid, ng-invalid, ng-dirty…) via provideSignalFormsConfig.

  • Interop with Reactive Forms via compatForm and SignalFormControl, allowing you to migrate incrementally without rewriting the world.

4. The New @Service() Decorator

One of those ergonomic improvements you'll be thankful for every day. @Service() shortens the most common injection case — that endlessly repeated @Injectable({ providedIn: 'root' }):

import { Service } from '@angular/core';

@Service()
export class FlightClient {
  // Provided in root by default. The intention is explicit.
}

Enter fullscreen mode Exit fullscreen mode

If you don't want the automatic root provision, turn it off with autoProvided: false and provide it manually (in app.config.ts, in the component, or in the route):

@Service({ autoProvided: false })
export class TabRegistry {}

Enter fullscreen mode Exit fullscreen mode

Important: @Service() does not retire @Injectable(). It is a shortcut for the most frequent case. Wherever you use more sophisticated provider configurations, @Injectable() remains the right tool. In a Clean Architecture, @Service() makes the infrastructure adapter layer much leaner — but use it with discretion, not as a blind substitute.

5. injectAsync: Lazy Dependency Injection

This is a gift for anyone fighting bundle sizes and startup times. With injectAsync, you inject a dependency only when it is truly needed — ideal for services that load heavy libraries and only step in after a specific user action:

import { injectAsync } from '@angular/core';

@Component({ /* ... */ })
export class CheckinPage {
  private readonly upgradeService = injectAsync(() =>
    import('./upgrade-service').then((m) => m.UpgradeService),
  );

  protected async upgrade(): Promise<void> {
    const service = await this.upgradeService();
    service.upgrade(/* ... */);
  }
}

Enter fullscreen mode Exit fullscreen mode

The import — and therefore, the bundle loading — only happens on the first call. To avoid the delay of that first time, you can pre-load it with the prefetch option combined with onIdle (which loads when the browser is idle):

import { injectAsync, onIdle } from '@angular/core';

private readonly upgradeService = injectAsync(
  () => import('./upgrade-service').then((m) => m.UpgradeService),
  { prefetch: onIdle }, // loads when the browser is idle
);

Enter fullscreen mode Exit fullscreen mode

Just remember: for lazy load to work, the injected service needs to be auto-provided (via @Service() or @Injectable({ providedIn: 'root' })).

6. debounced: Native Debounce for Signals

Signals, by nature, know nothing about time — they have no debounceTime or throttle. Angular 22 solves this with the debounced function, which creates a Resource whose value is updated with the defined delay:

import { debounced } from '@angular/core';

const filter = signal('');
const debouncedFilter = debounced(filter, 300); // 300ms
effect(() => console.log(debouncedFilter.value()));

Enter fullscreen mode Exit fullscreen mode

For forms, debounce is already built into Signal Forms — debounced covers everything else.

7. Incremental Hydration Enabled by Default

Focusing on performance: incremental hydration is now enabled by default via provideClientHydration(). For applications with SSR, this means a real gain in the initial load, hydrating components on demand instead of everything all at once. If for some reason you don't want it, you can explicitly turn it off with withNoIncrementalHydration() — and there is a migration schematic to help you.

8. HttpClient Now Uses FetchBackend by Default

A discreet change, but with a real impact on migration: the HttpClient now uses the Fetch API by default, meaning withFetch() is deprecated and can be removed.

The catch lies in request progress tracking. The old reportProgress was replaced by two dedicated options — and upload progress requires XHR:

// Download (works with Fetch)
http.get('/large-file', { reportDownloadProgress: true, observe: 'events' });

// Upload (requires withXhr())
http.post('/upload', file, { reportUploadProgress: true, observe: 'events' });

Enter fullscreen mode Exit fullscreen mode

If you use reportUploadProgress with the FetchBackend, Angular intentionally throws an exception, signaling that you need withXhr(). Fortunately, ng update adds withXhr() automatically to preserve existing behavior.

9. For Those Who Live on Microfrontends

This section rarely appears in generic summaries, but it's exactly what matters for those orchestrating Module Federation and distributed shells. Angular 22 brought improvements tailored for this scenario:

  • ApplicationRef.bootstrap with config: bootstrap() now accepts a configuration object analogous to createComponent, allowing you to spin up a microfrontend on demand in a specific area of the page:
appRef.bootstrap(MyComponent, { hostElement: document.querySelector('#root')! });

Enter fullscreen mode Exit fullscreen mode
  • Bootstrap under Shadow Roots: you can start Angular directly inside a shadow root, with styles correctly registered in the SharedStylesHost. Another step toward clean integration with Web Components.

  • Wildcard routes with segments ahead and behind ('foo/**/bar'): previously only achievable with a custom path matcher. Perfect for shells that need to load the correct microfrontend based on a URL pattern.

  • Auto cleanup of Environment Injectors per route (withExperimentalAutoCleanupInjectors): services provided at the route level are finally destroyed upon leaving it, instead of living until the application closes. Still experimental, but it resolves an old pain point regarding instance leaks (memory leaks).


Best Practices for Migrating Safely

Migrating scalable applications requires strategy. A pragmatic roadmap:

  1. Run ng update without fear of OnPush. It applies Eager where there was no explicit strategy, so nothing breaks. Afterward, migrate component by component to OnPush and Signals, starting with the "hottest" presentation layers.

  2. Clean up the dependency injection layer. Use your IDE's refactoring tools to swap the simpler @Injectable({ providedIn: 'root' }) decorators for @Service() — and keep @Injectable() where there are custom providers.

  3. Adopt injectAsync + onIdle to slim down the initial bundle. Identify heavy services that are not needed at startup and load them on demand, with prefetch set to idle.

  4. Migrate simple RxJS GETs to httpResource. Where you previously used RxJS just to resolve a GET request, httpResource drastically reduces cognitive complexity — without losing race condition handling.

  5. Check your HttpClient. Remove withFetch() (now redundant) and ensure withXhr() is present where you depend on upload progress. ng update helps, but be sure to review it.

  6. Validate hydration in SSR. Incremental Hydration comes enabled; test the behavior and use withNoIncrementalHydration() if any specific flow needs the old mode.

For those just starting — where to truly begin: do not try to migrate everything at once. Run ng update, ensure the app keeps working (thanks to the Eager fallback), and only then pick a simple component to convert to Signals + OnPush. Feel the gain, build confidence, repeat. A good migration is a boring and gradual one — not a heroic one.


Final Considerations

Angular 22 is proof that Google's framework has rebuilt itself from the inside out. That reputation of being "heavy" and "verbose" has been left behind: today we have fine-grained reactivity, true zone-less functionality, and a top-tier Developer Experience. Those who held off on updates in recent months now receive a stable foundation that allows migrating to a Signals-based development flow without relying on experimental APIs.

If you are just starting now, the good news is that you are entering a much simpler and more straightforward Angular than the one from a few years ago. And if you already build complex and scalable products, rarely have we had such powerful tools at our disposal.

Did you like the analysis? Apply these ideas in your next refactor and let me know how it went. To continue discussing frontend architecture, technical content, and the development world, subscribe to the GhabDev channel on YouTube and follow my upcoming articles here.

Happy coding! 💻🚀


📚 Glossary — Technical Terms from the Article

For those just starting (or wanting a review). In alphabetical order.

  • Asynchronous: Something that doesn't happen instantly — you ask the server for data and the response arrives "later." The code needs to know how to wait.

  • Boilerplate: Repetitive and ceremonial code that you write constantly without it adding real logic. Less boilerplate = less boring code.

  • Bootstrap: "Starting up" the Angular application — the moment the framework initializes and renders the first component on the page.

  • Bundle: The final JavaScript package the browser downloads to run your app. The larger it is, the slower the first load will be.

  • Change detection: The mechanism that decides when and where Angular re-renders the screen after some data changes.

  • Clean Architecture: A way to organize code into well-separated layers (business rules in the center, details like databases and APIs on the edges). The goal is to be able to swap out a technology without messing up the rest.

  • Debounce: A technique used to wait for the user to stop before reacting. E.g.: in a search, instead of calling the API on every letter typed, you wait for 300ms of silence and only then search. Saves requests and improves the experience.

  • Declarative: You describe what you want ("this field is required") instead of writing step-by-step how to do it. More readable.

  • Decorator: That @ before a class (@Component, @Service). It’s an annotation that tells Angular "treat this class in a special way."

  • dirty: Marks if the user has interacted with a form field. Useful for only showing the "required field" error after they've interacted, instead of shouting errors while the form is still blank.

  • Eager / Default: The "old and expensive" change detection strategy — every cycle Angular scans the entire component tree looking for changes. It works, but wastes processing power. In Angular 22 it became Eager and is deprecated.

  • Enterprise: Large, corporate applications with many teams and years of maintenance ahead.

  • Fetch vs. XHR: Two ways for the browser to make HTTP requests. XHR (XMLHttpRequest) is the old way; Fetch is the modern one, Promise-based (cleaner), and better for streaming. The only thing Fetch still doesn't do well is report upload progress.

  • Strongly typed: TypeScript "knows" the exact shape of your data and warns you in the editor if you mess up a field, before running. Fewer bugs in production.

  • Framework: A structured "toolbox" that already solves common problems (routing, forms, requests) so you don't have to reinvent the wheel.

  • Hydration / Incremental Hydration: The process of "giving life" to the static HTML coming from the server — Angular attaches events and interactivity. Incremental means doing this little by little, only in the parts the user sees/uses, instead of everything all at once.

  • Dependency Injection (DI): Instead of your class creating the things it needs on its own (e.g.: an API service), it just asks and Angular delivers it. Makes the code easier to test and maintain.

  • Interop / Incremental migration: "Interop" is the ability of new code to talk to the old. "Incremental migration" is updating the project gradually, one piece at a time, without rewriting everything in a single, risky big bang.

  • Lazy loading: Instead of downloading everything at once, you delay the download of heavy parts until the moment they are actually used. The page opens faster.

  • Memory leak: When something keeps occupying memory even after it is no longer needed. In long-running apps, leaks accumulate and slow everything down.

  • Microfrontends: Dividing a large application into several independent "mini-apps", each handled by a different team, which come together on a single screen. It's the "microservices" of the frontend world.

  • Module Federation: The technology (from Webpack) that allows these mini-apps to share code and be loaded at runtime.

  • ng update: An Angular CLI command that updates your project from one version to another, applying automatic migrations whenever possible.

  • onIdle / prefetch: "Prefetch" means loading something before you need it, quietly. onIdle does this during moments when the browser is idle (using the requestIdleCallback API). That way, when the user clicks, everything is ready.

  • OnPush: An "economical" change detection strategy — Angular only checks the component when it receives a new input, an event, or a Signal changes. Result: more performance.

  • providedIn: 'root': Says there is a single instance of the service for the entire app (singleton pattern). Previously you wrote this by hand; now @Service() assumes it by default.

  • Race condition: When the user types fast and fires off several searches in a row, the response from an older search might arrive later and overwrite the right result with a wrong one. httpResource solves this on its own.

  • Reactive / Reactivity: The UI reacts on its own when the data changes, without you manually commanding "update the screen."

  • Reactive Forms vs. Template-Driven: The two older ways of building forms in Angular. Reactive Forms builds the form in TypeScript (more control, more verbose); Template-Driven builds it in HTML (simpler, less powerful). Signal Forms unify the best of both.

  • HTTP Request: The request your app makes to a server ("give me the flight list"). The server responds with the data.

  • Resource: A "smart package" that makes the request, stores the result, and hands you three states on a silver platter: loading, error, and the final value.

  • RxJS / switchMap: RxJS is a reactive programming library based on streams (data flows over time). switchMap is one of its operators that, when a new request arrives, cancels the previous one — exactly what prevents messy results.

  • Schema: The "blueprint" that describes the validation rules for a form (which fields, what limits). Zod and Valibot are popular libraries for this.

  • Schematic: An Angular automation script that applies changes to your code for you (renaming, moving configs, etc.) during a migration.

  • Service: A class that concentrates reusable, viewless logic — e.g.: fetching data, calculating, holding state. Multiple components can share the same service.

  • Shell: The "shell" app that orchestrates and loads the microfrontends within itself.

  • Signals: The modern way Angular holds a value that automatically notifies whoever depends on it when it changes — like a spreadsheet where cell B recalculates itself when A changes.

  • SSR (Server-Side Rendering): The server puts together the ready-made HTML and sends it to the browser. The user sees the page fast, before the JavaScript even loads. Good for performance and SEO.

  • Startup: The time between the user opening the page and it becoming usable.

  • Web Components / Shadow Root: Web Components are UI components that work in any framework. The shadow root is an isolated "bubble" that protects the component's styles from leaking out (and from being interfered with) — great when multiple teams mix code.

  • Wildcard (): A "joker" in the route definition that matches any path. Useful when you don't know the exact URL beforehand, only the pattern.

  • Zone-less / Zone.js: Zone.js is a library that historically "watched" everything happening in the application to know when to update the screen. Zone-less is Angular working without it — something made possible thanks to Signals, and much faster.

Technical sources: official Angular v22 announcement (angular.dev) and the detailed analysis by Manfred Steyer (ANGULARarchitects), both from June 2026.


Originally published on ZyVOP

💡 For more articles like this, subscribe to the ZyVOP newsletter!

Top comments (0)