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.
ComponentFactoryResolverInject 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.
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.
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.
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: 0Styled Component
This component injects a <style> tag into the DOM.
When destroyed, its styles are removed from the DOM.
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.
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.
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
ComponentFactoryResolverPass class directly to createComponent()ComponentFactoryNo longer neededChangeDetectorRef.checkNoChanges()fixture.detectChanges() in testscreateNgModuleRef()createNgModule()appRef.bootstrap(Comp, any)appRef.bootstrap(Comp, nonNullableEl)Styles persist after destroyStyles now removed from DOM on destroyTestability via Zone.jsPendingTasks for stability (works zoneless)bootstrap(Comp, element)bootstrap(Comp, { hostElement, bindings, directives })