Angular 22 Is Here: Signal Forms, Stable Resources, and the Signal-First Era
Angular TypeScript Signals Web Development Frontend

Angular 22 Is Here: Signal Forms, Stable Resources, and the Signal-First Era

D. Rout

D. Rout

June 12, 2026 10 min read

On this page

Angular 22 dropped on June 3, 2026, and it feels different from every release before it. This isn't a feature dump — it's a consolidation. The two-year experiment with Signals is over. Signal Forms and the Resource API (resource(), httpResource()) are now fully production-stable, OnPush is the new default change detection strategy, and a fresh set of DI ergonomics (@Service, injectAsync) make the framework cleaner than it's ever been.

If you've been holding off on adopting signal-based patterns in your Angular apps, this is the release that gives you the green light. This tutorial walks through every major change with real code, a migration reference table, and a working demo project you can clone today.

GitHub repo: angular22-whats-new — three runnable demos covering httpResource(), Signal Forms, and injectAsync().


Prerequisites {#prerequisites}

Before diving in, make sure your environment meets Angular 22's requirements:

  • Node.js 22+ (Node 20 support is dropped in v22)
  • TypeScript 6+ (TypeScript 5.x is no longer supported)
  • **Angular CLI 22**: `npm install -g @angular/cli@22`
  • Familiarity with Angular Signals basics — if you're new to signals, check out our Angular Signals deep-dive first.

1. Stable Signal Forms: No More Experimental Label {#signal-forms-stable}

Signal Forms were introduced as experimental in Angular 21. As of Angular 22, they are fully stable and production-ready. The API has not changed materially — but the guarantee has: no more breaking changes without a deprecation cycle.

What Signal Forms look like

import { Component, computed } from '@angular/core';
import { formGroup, formControl } from '@angular/forms';

@Component({
  selector: 'app-product-search',
  standalone: true,
  template: `
    <div [formGroup]="searchForm">
      <input [formControl]="searchForm.controls.query" placeholder="Search…" />
      <select [formControl]="searchForm.controls.category">
        <option value="">All</option>
        <option value="electronics">Electronics</option>
      </select>
    </div>
    <p>Valid: {{ searchForm.valid() }}</p>
    <p>Value: {{ searchForm.value() | json }}</p>
  `,
})
export class ProductSearchComponent {
  // No FormBuilder, no constructor injection — just a function call
  readonly searchForm = formGroup({
    query: formControl('', { required: true, minLength: 2 }),
    category: formControl(''),
  });

  // Derived signal — automatically recomputes when the form value changes
  readonly filteredResults = computed(() => {
    const { query } = this.searchForm.value();
    return query.length >= 2 ? PRODUCTS.filter(p => p.name.includes(query)) : [];
  });
}

Notice what's gone: no FormBuilder, no constructor injection, no valueChanges subscription. searchForm.valid() and searchForm.value() are plain signals — they compose with computed(), effect(), and everything else in the signal graph.

What's new in v22 Signal Forms

Angular 22 also ships several additions to the Signal Forms API:

  • Submission API — a first-class submit() handler with built-in pending/error state signals
  • validateStandardSchema — integrate Zod, Valibot, or any Standard Schema validator declaratively
  • Conditional CSS classes — bind class names directly to form state signals
  • Reactive Forms interop — bridge legacy AbstractControl instances into the signal graph for gradual migration

2. Stable Resource API: resource(), rxResource(), httpResource() {#resource-api-stable}

The entire Resource API family graduates to stable in Angular 22. All three functions are production-ready:

Function Use case
resource() Custom async loader with full control
rxResource() RxJS-based loader (returns Observable)
httpResource() Typed HTTP GET shorthand via HttpClient

httpResource() in practice

httpResource() is the fastest path for reactive data fetching. It accepts a signal-returning URL factory and wires loading, error, and value state automatically:

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

interface User {
  id: number;
  name: string;
  email: string;
}

@Component({
  selector: 'app-user-profile',
  standalone: true,
  template: `
    <input type="number" [value]="userId()" min="1" max="10"
           (input)="userId.set(+$any($event.target).value)" />

    @if (userResource.isLoading()) {
      <p>Loading…</p>
    } @else if (userResource.error()) {
      <p class="error">Failed to load user</p>
    } @else {
      <h2>{{ userResource.value()?.name }}</h2>
      <p>{{ userResource.value()?.email }}</p>
    }
  `,
})
export class UserProfileComponent {
  readonly userId = signal(1);

  // Re-fetches automatically whenever userId() changes
  readonly userResource = httpResource<User>(
    () => `/api/users/${this.userId()}`
  );
}

When userId changes, the previous request is automatically cancelled and a fresh one is issued. No switchMap, no takeUntilDestroyed, no manual loading flag.

Custom resource() with reload

For cases where httpResource() is too thin, resource() gives you full control:

import { resource, signal } from '@angular/core';

const searchQuery = signal('angular');

const searchResults = resource({
  request: () => ({ q: searchQuery() }),
  loader: async ({ request, abortSignal }) => {
    const res = await fetch(`/api/search?q=${request.q}`, { signal: abortSignal });
    if (!res.ok) throw new Error('Search failed');
    return res.json();
  },
});

// Trigger a manual reload (e.g. after a mutation)
searchResults.reload();

The abortSignal passed to the loader is automatically triggered when the request becomes stale — your fetch() calls are cancelled for free.


3. OnPush Is Now the Default {#onpush-default}

This is the change that will affect every Angular developer upgrading from v21. New components now use OnPush change detection by default. The old Default (now renamed Eager) strategy must be opted into explicitly.

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

// Angular 22: OnPush is implicit — no need to specify it
@Component({ selector: 'app-my', template: `…` })
export class MyComponent {}

// If you need the old behavior, set Eager explicitly
@Component({
  selector: 'app-legacy',
  changeDetection: ChangeDetectionStrategy.Eager,
  template: `…`,
})
export class LegacyComponent {}

What happens during ng update? Any existing component that relied on the old implicit default has ChangeDetectionStrategy.Eager added automatically. You won't get regressions — but you should review those components and migrate them to OnPush + signals over time.


4. @Service Decorator {#service-decorator}

Every Angular developer has typed @Injectable({ providedIn: 'root' }) thousands of times. Angular 22 ships @Service() as a cleaner, more intentional alternative:

// Before — Angular 21 and earlier
import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class UserStore { … }

// After — Angular 22
import { Service } from '@angular/core';

@Service()
export class UserStore { … }

@Service() is a strict superset of @Injectable({ providedIn: 'root' }) for the common case. It creates a tree-shakeable root-level singleton and enforces the inject() function over constructor injection. @Injectable isn't going anywhere — use it when you need a non-root scope or constructor injection for compatibility.

The Angular CLI now generates @Service() by default with ng generate service. You can revert to @Injectable with ng generate service --injectable.


5. injectAsync(): Lazy-Loaded Services {#inject-async}

injectAsync() brings true service-level code splitting to Angular. Large dependencies — PDF exporters, analytics SDKs, chart libraries — can now stay out of the initial bundle until they're actually needed:

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

@Component({
  selector: 'app-checkout',
  standalone: true,
  template: `
    <button (click)="placeOrder()">Complete Order</button>
    @if (orderPlaced()) { <p>✅ Order placed!</p> }
  `,
})
export class CheckoutComponent {
  readonly orderPlaced = signal(false);

  // AnalyticsService is lazy — not in the initial bundle
  // { onIdle: true } prefetches when the browser is idle
  private readonly analytics = injectAsync(
    () => import('../services/analytics.service').then(m => m.AnalyticsService),
    { onIdle: true }
  );

  async placeOrder() {
    const analytics = await this.analytics(); // loads on first call
    analytics.track('checkout_completed', { timestamp: Date.now() });
    this.orderPlaced.set(true);
  }
}

The onIdle option kicks off prefetching via requestIdleCallback while the user is reading the page, so the first interaction is near-instant even though the code wasn't in the initial bundle.


6. Other Notable Changes {#other-changes}

Template syntax expansions

Angular 22 expands what you can write directly in templates:

<!-- Inline arrow functions -->
<button (click)="items.update(list => [...list, newItem()])">Add</button>

<!-- Spread syntax -->
<app-card [config]="{ ...baseConfig, title: item.title }" />

<!-- Template comments -->
<!-- @component: ProductCard | owner: @deepakrout -->
<div class="card">…</div>

<!-- Multiple @switch cases -->
@switch (status) {
  @case ('pending', 'processing') { <span class="badge yellow">In Progress</span> }
  @case ('complete') { <span class="badge green">Done</span> }
}

Route parameter inheritance

paramsInheritanceStrategy now defaults to 'always', meaning child routes automatically inherit all parent route params. This eliminates the route.parent?.parent?.snapshot.params pattern in deeply nested routes.

HTTP client uses Fetch by default

The HttpClient now uses the browser's native fetch() API as its default transport. Zone.js patching of XMLHttpRequest is no longer in the hot path for HTTP calls.

TypeScript 6 required, Node 20 dropped

Angular 22 requires TypeScript 6 and drops Node 20 support. Node 22 and 26 are both supported.


Angular 22 Feature Reference {#reference-table}

Feature Status Notes
Signal Forms ✅ Stable Production-ready; Submission API + Zod interop included
resource() / httpResource() ✅ Stable Full replacement for RxJS async patterns
Angular Aria ✅ Stable Reactive accessibility utilities
OnPush as default ✅ Stable ng update adds Eager to existing components
@Service() decorator ✅ Stable Replaces @Injectable({ providedIn: 'root' })
injectAsync() ✅ Stable Service-level code splitting with onIdle prefetch
HTTP Client via Fetch ✅ Stable Native fetch() replaces XHR as default transport
Route param inheritance ✅ Stable paramsInheritanceStrategy: 'always' is now the default
debounced() signal 🧪 Experimental Debounced signal utility
WebMCP 🧪 Experimental Expose Angular services as AI agent tools
@boundary error boundaries 👀 Preview Template-level error boundaries (v22.1 expected)
TypeScript 6 ⚠️ Required TypeScript 5.x no longer supported
Node 20 ❌ Dropped Node 22+ required

Upgrading from Angular 21 {#upgrade}

The upgrade is straightforward for most apps:

# Update the CLI globally
npm install -g @angular/cli@22

# Migrate your project (runs automatic codemods)
ng update @angular/core@22 @angular/cli@22

Key things the migration does automatically:

  • Adds ChangeDetectionStrategy.Eager to components that relied on the previous implicit default, preserving behavior
  • Updates HttpClient imports for the new Fetch backend

After migrating, you can incrementally adopt OnPush + signals component-by-component at your own pace.


What's Next {#whats-next}

Angular 22 is a launchpad, not a destination. A few directions worth watching:

  1. Migrate your Reactive Forms to Signal Forms — now that Signal Forms are stable, it's the right time to start. Our Signal Forms tutorial covers the migration path end-to-end.
  2. Adopt httpResource() in your data services — replace BehaviorSubject + switchMap patterns with reactive httpResource() calls. Our resource() API tutorial is now fully up to date for v22 stable.
  3. Explore WebMCP — the experimental WebMCP layer lets you expose Angular forms and services as typed tools that AI agents can call directly. It's early, but it's the most forward-looking addition in v22.
  4. Audit your bundle with injectAsync() — identify large services that could be lazy-loaded to reduce initial bundle size and improve Time to Interactive.

Further Reading {#further-reading}


Try the Demo {#demo}

The full working project for this post lives at angular22-whats-new. Clone it, run npm install && npm start, and you'll have three interactive demos running locally:

  • /user-profilehttpResource() reacting to a signal-driven user ID picker
  • /product-search — Signal Forms with a computed() derived result list
  • /checkoutinjectAsync() keeping AnalyticsService out of the initial bundle

Angular 22 is the release where the framework's signal-first vision becomes the default. The patterns you've been reading about for two years are now the safe, supported, production way to build Angular apps. There's no better time to upgrade.

Share

Comments (0)

Join the conversation

Sign in to leave a comment on this post.

No comments yet. to be the first!