Angular 22

Breaking Changes in Angular 22

Angular 22 removes several deprecated APIs. Here's what changed and how to migrate your code.

RemovedComponentFactoryResolver & ComponentFactory

You no longer need a factory to create components dynamically. Pass the component class directly to ViewContainerRef.createComponent() or use the standalone createComponent() function.

Before (Angular ≤21)→After (Angular 22)
ComponentFactoryResolver

Inject resolver, create factory, then create component

vcr.createComponent(MyComponent)

Pass the class directly , no factory needed

Try it , create a component without a factory:

// BEFORE (Angular ≤21) ,  ComponentFactoryResolver
constructor(
  private resolver: ComponentFactoryResolver,
  private vcr: ViewContainerRef
) {}

createDynamic() {
  const factory = this.resolver.resolveComponentFactory(MyComponent);
  this.vcr.createComponent(factory);
}

// AFTER (Angular 22) ,  pass the class directly
private vcr = inject(ViewContainerRef);

createDynamic() {
  this.vcr.createComponent(MyComponent);
}

// Or use the standalone function:
import { createComponent } from '@angular/core';
const ref = createComponent(MyComponent, { environmentInjector });

RemovedChangeDetectorRef.checkNoChanges()

This method was removed from ChangeDetectorRef. In tests, use fixture.detectChanges() instead. Angular's dev mode already performs change detection checks automatically.

Before→After
fixture.changeDetectorRef.checkNoChanges()
fixture.detectChanges()
// BEFORE (Angular ≤21)
it('should update', () => {
  component.value = 'new';
  fixture.changeDetectorRef.checkNoChanges(); // ❌ Removed
});

// AFTER (Angular 22)
it('should update', () => {
  component.value = 'new';
  fixture.detectChanges(); // ✅ Use this instead
  expect(fixture.nativeElement.textContent).toContain('new');
});

RemovedcreateNgModuleRef()

Renamed to createNgModule(). The function signature is identical , just update the import name.

Before→After
createNgModuleRef(Module, injector)
createNgModule(Module, injector)
// BEFORE (Angular ≤21)
import { createNgModuleRef } from '@angular/core';
const ref = createNgModuleRef(SomeModule, injector); // ❌ Removed

// AFTER (Angular 22)
import { createNgModule } from '@angular/core';
const ref = createNgModule(SomeModule, injector); // ✅

StricterappRef.bootstrap() Typing

The second argument of appRef.bootstrap() no longer accepts any. The element must be non-nullable , use a null check or the non-null assertion operator.

Before (accepted any)→After (non-nullable)
appRef.bootstrap(Comp, getElementById('root'))

No error even if getElementById returns null

appRef.bootstrap(Comp, el!)

Must ensure the element is non-nullable

// BEFORE (Angular ≤21) ,  accepted any
appRef.bootstrap(SomeComponent, document.getElementById('root'));
// No error even if getElementById returns null

// AFTER (Angular 22) ,  stricter typing
const el = document.getElementById('root');
if (el) {
  appRef.bootstrap(SomeComponent, el); // ✅ non-nullable
}

// Or use non-null assertion if you're sure:
appRef.bootstrap(SomeComponent, document.getElementById('root')!);

Changed Style Cleanup on Component Destroy

Angular 22 removes <style> tags from the DOM when their host component is destroyed. Previously, styles persisted in <head> forever, potentially leaking to unrelated elements.

<style> tags in DOM: 0

Styled Component

This component injects a <style> tag into the DOM.

When destroyed, its styles are removed from the DOM.

Before (Angular ≤21)→After (Angular 22)

Component destroyed → <style> stays in DOM

Styles can leak to other elements

Component destroyed → <style> removed

Clean DOM, no style leaks

// Angular 22: Component styles are removed from the DOM on destroy
// This means <style> tags injected by a component are cleaned up
// when the component is destroyed.

// Before (Angular ≤21):
// - Component is destroyed
// - Its <style> tag stays in <head> forever
// - Styles could "leak" and affect unrelated elements

// After (Angular 22):
// - Component is destroyed
// - Its <style> tag is removed from <head>
// - Cleaner DOM, no style leaks

// ⚠️ If you relied on styles persisting after component destroy,
// move those styles to a global stylesheet instead.

ChangedTestability Uses PendingTasks

Angular 22's Testability service no longer relies on Zone.js to determine app stability. It now uses PendingTasks, which works in both zoned and zoneless apps. Call pendingTasks.add() to signal async work, and the returned cleanup function to mark it complete.

Before (Zone.js)→After (PendingTasks)

Stability tracked via Zone.js macrotask queue

Only worked with zone.js loaded

Stability tracked via PendingTasks

Works with and without Zone.js

// BEFORE (Angular ≤21) — Testability tracked Zone.js macrotasks
// Protractor relied on Zone.js to know when Angular was "stable"

// AFTER (Angular 22) — Testability uses PendingTasks internally
// Works with AND without Zone.js (zoneless apps!)

// ── In your app code: use PendingTasks ──
const pendingTasks = inject(PendingTasks);
const cleanup = pendingTasks.add();

// App is now "unstable" — e2e tests will wait
await doAsyncWork();

// Mark as complete — app becomes "stable" again
cleanup();

// Check stability reactively via the built-in signal:
pendingTasks.hasPendingTasks();   // true while tasks pending

// ── In e2e tests: Testability reads PendingTasks under the hood ──
// (Testability is consumed by test frameworks externally,
//  not injected in app components)
const testability = getAllAngularTestabilities()[0];
testability.isStable();           // false while tasks pending
testability.whenStable(() => {    // callback fires when all done
  console.log('App is stable!');
});

Newbootstrap() & createComponent() Config Object

ApplicationRef.bootstrap() and createComponent() now accept a configuration object with hostElement, directives, and bindings. Use inputBinding(), outputBinding(), and twoWayBinding() to wire up inputs and outputs programmatically — no template required. Signals passed to inputBinding() stay reactive.

Before (Angular ≤21)→After (Angular 22)
appRef.bootstrap(Comp, element)

Only accepted a host element as second arg

No way to bind inputs/outputs at creation

appRef.bootstrap(Comp, { hostElement, bindings, directives })

Config object with reactive bindings

inputBinding(), outputBinding(), twoWayBinding()

Try it — create a component with programmatic input/output bindings:

// NEW in Angular 22: bootstrap() accepts a config object
// with hostElement, directives, and bindings

import { inputBinding, outputBinding, twoWayBinding } from '@angular/core';

// ApplicationRef.bootstrap() with config:
appRef.bootstrap(MyComponent, {
  hostElement: document.getElementById('app')!,
  directives: [LogDirective, { type: TooltipDirective, bindings: [
    inputBinding('text', () => 'Hello!')
  ]}],
  bindings: [
    inputBinding('title', () => 'My App'),
    outputBinding('closed', () => console.log('closed')),
  ],
});

// Also works with createComponent():
const ref = createComponent(MyCard, {
  environmentInjector,
  bindings: [
    inputBinding('title', titleSignal),  // reactive via signal!
    inputBinding('message', () => 'Hello'),
    outputBinding('dismissed', () => handleDismiss()),
  ],
});

// Two-way binding with a writable signal:
const selected = signal('option-a');
createComponent(MySelect, {
  environmentInjector,
  bindings: [twoWayBinding('value', selected)],
});

Quick Migration Reference

Removed / ChangedReplacement
ComponentFactoryResolverPass class directly to createComponent()
ComponentFactoryNo longer needed
ChangeDetectorRef.checkNoChanges()fixture.detectChanges() in tests
createNgModuleRef()createNgModule()
appRef.bootstrap(Comp, any)appRef.bootstrap(Comp, nonNullableEl)
Styles persist after destroyStyles now removed from DOM on destroy
Testability via Zone.jsPendingTasks for stability (works zoneless)
bootstrap(Comp, element)bootstrap(Comp, { hostElement, bindings, directives })