Angular 22 released June 3, 2026. It's not one big feature — it's the moment several parallel efforts across the past two years all landed stable at the same time. Here's what actually changed and what it means for your code.

1. OnPush Is Now the Default

This is the change that touches the most existing code. Before Angular 22, a component with no changeDetection property used ChangeDetectionStrategy.Default — checking the whole tree on every event. Now it uses OnPush.

// Angular 22 — OnPush is the default, no config needed
@Component({
  selector: 'app-counter',
  standalone: true,
  template: `<h2>{{ count() }}</h2>`
})
export class CounterComponent {
  count = signal(0) // signals + OnPush: updates just work
}

The Angular team handled backward compatibility cleanly. A new ChangeDetectionStrategy.Eager value preserves the old "check always" behavior, and the CLI migration adds it automatically to your existing components:

// The migration adds this to components that relied on the old default
@Component({
  selector: 'app-legacy',
  changeDetection: ChangeDetectionStrategy.Eager, // ← migration-added
  template: `...`
})
export class LegacyComponent {}

After migration, Eager in your codebase becomes a to-do list — each occurrence marks a component that hasn't been converted to signals + OnPush yet. Work through them at your own pace.

What this means practically: new components you write today are high-performance by default. If you build with signals (and you should), you won't need to think about change detection at all — signal reads automatically mark the view dirty when their value changes.

2. Signal Forms Go Stable

Signal Forms shipped as experimental in Angular 21. In Angular 22 they're stable — no more experimental warnings, and the API is part of the supported contract.

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',
  standalone: true,
  imports: [FormField],
  template: `
    <form (submit)="onSubmit($event)">
      <div class="mb-3">
        <label class="form-label fw-semibold">Email</label>
        <input type="email" class="form-control" [formField]="loginForm.email" />
        @if (loginForm.email().touched() && loginForm.email().invalid()) {
          @for (error of loginForm.email().errors(); track error) {
            <div class="invalid-feedback d-block">{{ error.message }}</div>
          }
        }
      </div>

      <div class="mb-4">
        <label class="form-label fw-semibold">Password</label>
        <input type="password" class="form-control" [formField]="loginForm.password" />
      </div>

      <button type="submit" class="btn w-100 text-white fw-semibold"
        style="background:#fd4766;"
        [disabled]="loginForm().invalid()">
        Sign In
      </button>
    </form>
  `
})
export class LoginComponent {
  loginModel = signal<LoginData>({ email: '', password: '' })

  loginForm = form(this.loginModel, (f) => {
    required(f.email,    { message: 'Email is required' })
    email(f.email,       { message: 'Enter a valid email' })
    required(f.password, { message: 'Password is required' })
  })

  onSubmit(event: Event) {
    event.preventDefault()
    if (this.loginForm().valid()) {
      console.log(this.loginModel()) // { email: '...', password: '...' }
    }
  }
}

What changed in Angular 22 vs 21:

  • Built-in error results are now properly typed (autocomplete instead of any)
  • FormField.parseErrors no longer recomputes without reason (better perf on large forms)
  • date and limit validators are now public stable API
  • Plain-object models now have first-class documentation and support

3. resource() and httpResource() Go Stable

The two reactive async primitives are now stable. They keep HTTP requests inside the signal graph — no moving back and forth between RxJS for every data fetch:

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

interface Template {
  id: number
  name: string
  framework: string
  price: number
}

@Component({
  selector: 'app-template-detail',
  standalone: true,
  template: `
    @if (template.isLoading()) {
      <div class="d-flex justify-content-center py-5">
        <div class="spinner-border" style="color:#fd4766;"></div>
      </div>
    } @else if (template.hasValue()) {
      <div class="card border-0 shadow-sm p-4">
        <span class="badge mb-2 text-white" style="background:#fd4766;">
          {{ template.value().framework }}
        </span>
        <h1 class="fw-bold">{{ template.value().name }}</h1>
        <p class="text-muted">${{ template.value().price }}</p>
      </div>
    }
  `
})
export class TemplateDetailComponent {
  templateId = signal(1)

  // Re-fetches automatically when templateId changes
  template = httpResource<Template>(
    () => `/api/templates/${this.templateId()}`
  )
}

Two important bug fixes landed with the stable release:

  • rxResource and httpResource no longer leak subscriptions — this was causing slow memory growth in long-running sessions
  • The URL sanitizer lookup is now case-insensitive, closing a scheme-bypass gap

4. @Service Decorator — Simpler DI

@Service() replaces @Injectable({ providedIn: 'root' }) for the common case — a root-scoped, tree-shakeable singleton:

// Before Angular 22
import { Injectable } from '@angular/core'

@Injectable({ providedIn: 'root' })
export class TemplateService {
  getAll() { return fetch('/api/templates').then(r => r.json()) }
}

// Angular 22 — same result, less ceremony
import { Service } from '@angular/core'

@Service()
export class TemplateService {
  getAll() { return fetch('/api/templates').then(r => r.json()) }
}

@Injectable isn't going away — use it when you need non-root scoping or explicit configuration. @Service() is just the shorthand for the 90% case.

5. injectAsync() — Lazy-Loaded Services

injectAsync loads a service only when it's first called — the bundler splits it into a separate chunk that downloads on demand:

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

@Component({
  selector: 'app-report',
  standalone: true,
  template: `
    <button class="btn text-white fw-semibold"
      style="background:#fd4766;"
      (click)="export()">
      Export PDF
    </button>
  `
})
export class ReportComponent {
  // PDFExporter is NOT downloaded until export() is first called
  private pdfExporter = injectAsync(() =>
    import('./pdf-exporter').then(m => m.PDFExporter)
  )

  async export() {
    const exporter = await this.pdfExporter()
    exporter.export()
  }
}

Prefetch on idle — start downloading before the user clicks, but don't block the initial load:

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

private pdfExporter = injectAsync(
  () => import('./pdf-exporter').then(m => m.PDFExporter),
  { prefetch: () => onIdle({ timeout: 2000 }) } // prefetch within 2 seconds of idle
)

One requirement: the service must be auto-provided (@Service() or @Injectable({ providedIn: 'root' })). Without that, Angular can't instantiate it after the dynamic import resolves.

6. Router: paramsInheritanceStrategy Defaults to 'always'

Before Angular 22, reading a grandparent route's :id in a deeply nested child required .parent?.parent?.snapshot.params chains. Now route parameters inherit from all parent routes by default:

// Before Angular 22 — accessing grandparent param was painful
export class OrderItemComponent implements OnInit {
  ngOnInit() {
    // Had to climb the tree manually
    const orderId = this.route.parent?.parent?.snapshot.params['orderId']
  }
}

// Angular 22 — grandparent :orderId available directly
export class OrderItemComponent implements OnInit {
  ngOnInit() {
    const orderId = this.route.snapshot.params['orderId'] // just works
  }
}

This is a breaking change. If your app relied on 'emptyOnly' behavior (where children with their own components didn't inherit parent params), restore it explicitly:

// Restore previous behavior if needed
provideRouter(routes, withRouterConfig({
  paramsInheritanceStrategy: 'emptyOnly'
}))

For most apps this is a quality-of-life improvement. Worth a deliberate check on nested routes before upgrading.

7. WebMCP — Experimental AI Integration

The most forward-looking feature in Angular 22. WebMCP (Web Model Context Protocol) lets your application register typed tools that an in-browser AI agent can call directly — no DOM scraping, no separate server:

Application-wide tools:

import { Service, inject, provideExperimentalWebMcpTools } from '@angular/core'
import { bootstrapApplication } from '@angular/platform-browser'

@Service()
class TemplateService {
  search(query: string) {
    return fetch(`/api/templates?q=${query}`).then(r => r.json())
  }
}

bootstrapApplication(AppComponent, {
  providers: [
    provideExperimentalWebMcpTools([
      {
        name: 'searchTemplates',
        description: 'Search Bootstrap and Angular admin dashboard templates.',
        inputSchema: {
          type: 'object',
          properties: {
            query: { type: 'string', description: 'Search query' }
          },
          required: ['query']
        },
        execute: (input) => {
          const service = inject(TemplateService)
          return service.search(input.query).then(results => ({
            content: [{ type: 'text', text: JSON.stringify(results) }]
          }))
        }
      }
    ])
  ]
})

Implicit tools from Signal Forms — the feature that makes WebMCP actually click. Add provideExperimentalWebMcpForms() and pass an experimentalWebMcpTool option to a form(). Angular reads the form's data model, generates a JSON schema automatically, connects validation, and hooks up the submission handler. No schema written by hand:

@Component({ /* ... */ })
export class UserRegistrationComponent {
  private model = signal({
    firstName: '',
    lastName: '',
    email: '',
  })

  userForm = form(
    this.model,
    (f) => {
      required(f.firstName, { message: 'First name is required.' })
      required(f.lastName,  { message: 'Last name is required.' })
      email(f.email,        { message: 'Enter a valid email.' })
    },
    {
      experimentalWebMcpTool: {
        name: 'registerUser',
        description: 'Registers a new user account.'
      },
      submission: {
        action: async (formValue) => {
          await fetch('/api/users', {
            method: 'POST',
            body: JSON.stringify(formValue)
          })
        }
      }
    }
  )
}

The AI agent sees firstName, lastName, email as typed fields with validation rules — can fill the form, see validation errors if it sends bad data, correct itself, and submit. The form is no longer just UI; it's a capability that both humans and agents can use.

Important constraints:

  • Strictly experimental — the WebMCP spec is still evolving, API may change outside major versions
  • Tool names must be unique across the app — registering the same name twice throws
  • Angular doesn't validate agent arguments against your schema — validate inside execute yourself
  • Async validators are not triggered by the implicit forms tool — handle them in the submission action
  • Currently needs Chrome behind a flag + a polyfill for development

8. linkedSignal Gets a Custom set Option

linkedSignal now accepts a set option that intercepts writes, letting you expose a slice of larger state as a standalone WritableSignal:

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

interface Task {
  id: number
  title: string
  status: 'todo' | 'in-progress' | 'done'
}

@Component({
  selector: 'app-task',
  standalone: true,
  template: `
    <!-- status-picker only knows WritableSignal<string> — doesn't need to know Task -->
    <app-status-picker [(status)]="status" />
  `
})
export class TaskComponent {
  task = signal<Task>({ id: 1, title: 'Build feature', status: 'todo' })

  // Exposes task().status as a writable view
  // Writes route back into task, keeping it as the single source of truth
  status = linkedSignal(() => this.task().status, {
    set: (newStatus) => this.task.update(t => ({ ...t, status: newStatus }))
  })
}

Summary Table

FeatureStatus in v22What changed
OnPush change detectionDefaultAuto migration adds Eager to existing components
Signal FormsStableNo more experimental label, typed errors, better perf
resource() / httpResource()StableMemory leak fix, case-insensitive URL sanitizer
@Service() decoratorStableShorthand for @Injectable({ providedIn: 'root' })
injectAsync()StableFirst-class lazy-loaded services with prefetch triggers
paramsInheritanceStrategyDefault changedNow 'always' — breaking change, check nested routes
linkedSignal set optionExpected v22.1Route writes back to source signal synchronously
WebMCPExperimentalForms and services as AI agent tools
Comments inside element tagsStable<!-- comment --> now allowed between attributes
Security hardeningStableSSRF protections, stricter sanitization, TransferCache fix

For new projects, start with Angular 22's defaults — you get OnPush, signals, and @Service() for free. For existing projects, run the migration schematic and then work through the Eager markers at whatever pace suits your team.

Frequently Asked Questions

Angular 22 makes OnPush the default change detection strategy, graduates Signal Forms, resource() and httpResource() from experimental to stable, introduces the @Service decorator and injectAsync() for lazy-loaded services, changes route paramsInheritanceStrategy to 'always' by default, and adds WebMCP as an experimental feature for exposing app capabilities to AI agents.
No. The Angular team shipped an automatic migration that adds ChangeDetectionStrategy.Eager (the new name for the old 'check always' default) to all your existing components. Your app keeps working exactly as before. New components you write get OnPush for free.
Yes. Signal Forms became stable in Angular 22 — the experimental warnings are gone and the API is now part of Angular's supported contract. If you used Signal Forms during the Angular 21 experimental window, the API you already know is unchanged.
WebMCP is an experimental Angular 22 feature that lets your application register structured tools that an AI agent built into the browser can call directly — without DOM scraping. Angular connects these tools to dependency injection and the component lifecycle, so they register and unregister automatically. The underlying Web Model Context Protocol spec is still evolving, so the API may change.

Related

Need a Full Angular + Bootstrap Admin Dashboard?

Marvel Angular Dashboard — Angular 21 + Bootstrap 5 with 50+ components and dark mode.

Browse Templates →

Use code FIRST30 for 30% off.