Migrating from Angular 17 to Angular 21: A Practical Guide
Migrating an Angular application across four major versions might seem like a daunting task, but the Angular team has invested heavily in backward compatibility and automatic migration tooling. Each version from 17 to 21 has introduced significant improvements: from modern control flow to stable Signals, from zoneless change detection to TypeScript 5.9 support.
In this tenth and final article of the Modern Angular series, we will tackle the migration step by step, covering every transition from Angular 17 all the way to version 21. This is not just about updating dependencies: each version brings new APIs, deprecations, and best practices that are essential to understand in order to fully leverage the framework.
What You Will Learn in This Article
- The roadmap of features introduced in Angular 18, 19, 20, and 21
- How to prepare your project before starting the migration
- Step-by-step procedure for each version jump (17→18→19→20→21)
ng updatecommands and handling breaking changes- Updating third-party dependencies (Angular Material, NgRx, etc.)
- How to run tests after each migration phase
- A complete checklist for validating the migration
Modern Angular Series Overview
| # | Article | Focus |
|---|---|---|
| 1 | Angular Signals | Fine-grained reactivity |
| 2 | Zoneless Change Detection | Removing Zone.js |
| 3 | New Templates @if, @for, @defer | Modern control flow |
| 4 | Standalone Components | NgModule-free architecture |
| 5 | Signal Forms | Reactive forms with Signals |
| 6 | SSR and Incremental Hydration | Server-Side Rendering |
| 7 | Core Web Vitals in Angular | Performance and metrics |
| 8 | Angular PWA | Progressive Web App |
| 9 | Advanced Dependency Injection | Tree-shakable DI |
| 10 | You are here → Migrating from Angular 17 to 21 | Migration guide |
1. Version Overview: From Angular 17 to 21
Before proceeding with the migration, it is essential to understand the key features introduced in each version. Angular follows a six-month release cycle: each major version brings new features, stabilization of experimental APIs, and occasional breaking changes.
Release Timeline and Key Features
| Version | Release | TypeScript | Key Features |
|---|---|---|---|
| Angular 17 | Nov 2023 | 5.2 - 5.3 | Control flow (@if, @for, @switch), @defer, Signals developer preview, new branding |
| Angular 18 | May 2024 | 5.4 - 5.5 | Zoneless developer preview, stable signal inputs/outputs, stable deferrable views |
| Angular 19 | Nov 2024 | 5.6 - 5.7 | Standalone default, incremental hydration, linkedSignal, resource API |
| Angular 20 | May 2025 | 5.8 | Stable zoneless, signal forms preview, improved effect(), authoring format |
| Angular 21 | Nov 2025 | 5.9 | Zoneless default for new projects, TypeScript 5.9, runtime performance |
As you can see, the common thread across the last four versions is clear: Signals as the central reactive primitive, removal of Zone.js, and architecture simplification through standalone components. Each migration builds on the foundations of the previous one.
2. Preparing for Migration
A successful migration begins well before the first ng update. The preparation
phase is crucial to avoid unexpected issues and reduce downtime. Here are the fundamental
steps.
Checking Compatibility
First, check the current project version and installed dependencies. Each Angular version requires a minimum Node.js and TypeScript version.
System Requirements by Version
| Angular | Min Node.js | Recommended Node.js | TypeScript | RxJS |
|---|---|---|---|---|
| 17.x | 18.13 | 20.x | 5.2 - 5.3 | 7.x |
| 18.x | 18.19 | 20.x / 22.x | 5.4 - 5.5 | 7.x |
| 19.x | 18.19 | 22.x | 5.6 - 5.7 | 7.x |
| 20.x | 20.x | 22.x | 5.8 | 7.x |
| 21.x | 20.x | 22.x | 5.9 | 7.x |
Pre-Migration Checklist
# 1. Check the current Angular version
ng version
# 2. Check Node.js version
node --version
# 3. Ensure the repository is clean
git status
# 4. Create a dedicated migration branch
git checkout -b feat/angular-21-migration
# 5. Run the current test suite for a baseline
npm test -- --watch=false
# 6. Production build for baseline verification
npm run build
# 7. Check for outdated dependencies
npm outdated
# 8. Check for vulnerabilities
npm audit
Golden Rule of Angular Migration
Never skip versions. Angular only supports migration one major version at a time. If you are on Angular 17, you must go through 18, then 19, then 20, and finally 21. Each step includes automatic schematics that update your code, and these are designed to work only between consecutive versions.
3. Angular 17 → 18: Control Flow and Signal Inputs
The first jump is often the most significant in terms of code refactoring. Angular 18 stabilized the modern control flow (@if, @for, @switch) introduced as a developer preview in Angular 17, and brought signal inputs and signal outputs to stability.
Update Commands
# Update Angular CLI and Core
ng update @angular/core@18 @angular/cli@18
# If using Angular Material
ng update @angular/material@18
# If using Angular CDK
ng update @angular/cdk@18
# Resolve any peer dependency conflicts
npm install --legacy-peer-deps
Automatic Control Flow Migration
Angular 18 includes a schematic that automatically converts structural directives
(*ngIf, *ngFor, *ngSwitch) to the new block syntax.
# Automatic control flow migration
ng generate @angular/core:control-flow
# Or, for a specific project in a workspace
ng generate @angular/core:control-flow --project=my-app
# After migration, review the changes
git diff
Before and After: Control Flow
<!-- *ngIf with else -->
<div *ngIf="user; else noUser">
Welcome, {{ user.name }}
</div>
<ng-template #noUser>
<p>Please log in</p>
</ng-template>
<!-- *ngFor with trackBy -->
<ul>
<li *ngFor="let item of items; trackBy: trackById">
{{ item.name }}
</li>
</ul>
<!-- *ngSwitch -->
<div [ngSwitch]="status">
<p *ngSwitchCase="'active'">Active</p>
<p *ngSwitchCase="'inactive'">Inactive</p>
<p *ngSwitchDefault>Unknown</p>
</div>
<!-- @if with @else -->
@if (user) {
<div>Welcome, {{ user.name }}</div>
} @else {
<p>Please log in</p>
}
<!-- @for with track -->
<ul>
@for (item of items; track item.id) {
<li>{{ item.name }}</li>
} @empty {
<li>No items found</li>
}
</ul>
<!-- @switch -->
@switch (status) {
@case ('active') { <p>Active</p> }
@case ('inactive') { <p>Inactive</p> }
@default { <p>Unknown</p> }
}
Signal Inputs and Outputs
// BEFORE (Angular 17) - classic decorators
@Component({ selector: 'app-user-card' })
export class UserCardComponent {
@Input() name: string = '';
@Input() age: number = 0;
@Output() selected = new EventEmitter<string>();
onClick(): void {
this.selected.emit(this.name);
}
}
// AFTER (Angular 18) - signal inputs/outputs
import { input, output } from '@angular/core';
@Component({ selector: 'app-user-card' })
export class UserCardComponent {
name = input<string>('');
age = input<number>(0);
selected = output<string>();
onClick(): void {
this.selected.emit(this.name());
}
}
Key Features of Angular 18
- Stable control flow: @if, @for, @switch and @defer fully stable
- Signal inputs/outputs: Stable
input()andoutput()APIs - Zoneless developer preview: First support for applications without Zone.js
- Fallback content for ng-content: Default content for content projection
- Route redirects with functions: Dynamic redirects based on logic
- Firebase App Hosting: New optimized hosting service for Angular
4. Angular 18 → 19: Standalone Default and Hydration
Angular 19 represents an important architectural shift: standalone components
become the default. The standalone: true flag is no longer necessary
because all components are standalone by default. Additionally, incremental
hydration reaches stability, and new reactive primitives like
linkedSignal and the resource API make their appearance.
Update Commands
# Update Angular CLI and Core
ng update @angular/core@19 @angular/cli@19
# If using Angular Material
ng update @angular/material@19
# Check and fix deprecation warnings
ng build 2>&1 | grep -i "deprecated"
Standalone by Default
The Angular 19 schematic automatically removes standalone: true from all
components, since it becomes the default behavior. If a component is NOT standalone, it
must now explicitly declare standalone: false.
// BEFORE (Angular 18) - explicit standalone
@Component({
standalone: true, // Explicitly required
selector: 'app-header',
imports: [RouterLink, NgOptimizedImage],
templateUrl: './header.component.html',
})
export class HeaderComponent {}
// AFTER (Angular 19) - standalone by default
@Component({
// standalone: true is implicit, removed by schematic
selector: 'app-header',
imports: [RouterLink, NgOptimizedImage],
templateUrl: './header.component.html',
})
export class HeaderComponent {}
linkedSignal and resource API
import { signal, linkedSignal, resource } from '@angular/core';
@Component({ selector: 'app-product-list' })
export class ProductListComponent {
// linkedSignal: derived signal with override capability
selectedCategory = signal('electronics');
currentPage = linkedSignal(() => {
// Reset to page 1 when category changes
this.selectedCategory();
return 1;
});
// resource API: reactive async data loading
products = resource({
request: () => ({
category: this.selectedCategory(),
page: this.currentPage()
}),
loader: async ({ request }) => {
const response = await fetch(
`/api/products?cat=${request.category}&page=${request.page}`
);
return response.json();
}
});
changePage(page: number): void {
this.currentPage.set(page);
}
}
Key Features of Angular 19
- Standalone default: All components are standalone by default
- Stable incremental hydration: Progressive hydration based on triggers (viewport, hover, timer)
- linkedSignal: Derived signal with overridable value (developer preview)
- resource API: Async data loading integrated with Signals
- Strict standalone enforcement: Build error for missing imports
- SSR improvements: Event replay for interactions during hydration
5. Angular 19 → 20: Stable Zoneless and Signal Forms
Angular 20 marks a historic milestone: zoneless change detection becomes stable. After nearly two years of developer preview, Angular applications can officially remove Zone.js with full stability guarantees. Additionally, signal forms make their appearance as a developer preview, proposing a new way to handle forms entirely based on Signals.
Update Commands
# Update Angular CLI and Core
ng update @angular/core@20 @angular/cli@20
# If using Angular Material
ng update @angular/material@20
# After the update, start the dev server
ng serve
Adopting Zoneless Change Detection
With Angular 20, you can safely remove Zone.js. The schematic does not do it automatically to avoid breakage, but this is the right time to adopt zoneless if you have not already.
// app.config.ts - Enable zoneless
import { ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideClientHydration } from '@angular/platform-browser';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
// Replaces provideZoneChangeDetection()
provideZonelessChangeDetection(),
provideRouter(routes),
provideClientHydration(),
]
};
# 1. Remove zone.js from dependencies
npm uninstall zone.js
# 2. Remove the polyfill from angular.json
# Find and remove "zone.js" from the "polyfills" array
# 3. Verify there are no direct imports
grep -r "zone.js" src/
Warning: Code Incompatible with Zoneless
Some common patterns stop working without Zone.js because they relied on monkey-patching of async APIs. Search for and fix these patterns in your code:
- Direct component property changes after
setTimeoutorsetInterval: usesignal()instead - Using
Promise.then()to update UI state: convert toresource()or usesignal.set() - Third-party libraries that modify state outside Angular: use
inject(ChangeDetectorRef)as a temporary workaround - Tests with
fakeAsync/tick: replace withawait fixture.whenStable()
Key Features of Angular 20
- Stable zoneless change detection:
provideZonelessChangeDetection()fully stable - Signal forms (developer preview): New signal-based form APIs
- Improved effect(): Optional synchronous execution, better lifecycle integration
- Stable linkedSignal: Derived signal with override now fully stable
- Experimental authoring format: New format for defining components (.ng files)
- Deprecated API removal: Cleanup of APIs deprecated since Angular 16-17
6. Angular 20 → 21: TypeScript 5.9 and Zoneless by Default
Angular 21 is the version that consolidates all the work from previous releases. New projects
generated with ng new are zoneless by default, TypeScript 5.9
brings improvements to type checking and developer experience, and the runtime receives
further performance optimizations.
Update Commands
# Update Angular CLI and Core
ng update @angular/core@21 @angular/cli@21
# If using Angular Material
ng update @angular/material@21
# Update TypeScript to 5.9
npm install typescript@5.9 --save-dev
# Verify the build
ng build
TypeScript 5.9: Relevant Features for Angular
// 1. Improved type narrowing in templates
// TypeScript 5.9 improves narrowing support
// in @if blocks of Angular templates
// 2. Stable import attributes
import data from './config.json' with { type: 'json' };
// 3. Compiler performance improvements
// Faster builds thanks to improved caching
// and reduced memory allocations
// 4. Improved utility types for Signals
import { Signal, WritableSignal } from '@angular/core';
// NoInfer<T> prevents unwanted inference
function createState<T>(
initial: T,
validator: (value: NoInfer<T>) => boolean
): WritableSignal<T> {
const state = signal(initial);
// ...validation logic
return state;
}
Key Features of Angular 21
- Zoneless by default:
ng newgenerates projects without Zone.js - TypeScript 5.9: Full support with improved type checking
- Runtime performance: Optimizations to change detection and rendering
- Improved signal forms: More mature and complete APIs
- Stable resource API: Async data loading fully stable
- NgModule-based code removal: Further deprecation of module-based APIs
7. ng update Step by Step: The Complete Procedure
Let us now look at the complete procedure for migrating from version 17 to 21, including handling breaking changes and dependency conflicts. Each step must be completed and verified before proceeding to the next.
#!/bin/bash
# Angular 17 -> 21 Migration Script
echo "=== STEP 1: Angular 17 -> 18 ==="
ng update @angular/core@18 @angular/cli@18
npm install
ng build
npm test -- --watch=false
git add -A && git commit -m "chore: migrate to Angular 18"
echo "=== STEP 2: Angular 18 -> 19 ==="
ng update @angular/core@19 @angular/cli@19
npm install
ng build
npm test -- --watch=false
git add -A && git commit -m "chore: migrate to Angular 19"
echo "=== STEP 3: Angular 19 -> 20 ==="
ng update @angular/core@20 @angular/cli@20
npm install
ng build
npm test -- --watch=false
git add -A && git commit -m "chore: migrate to Angular 20"
echo "=== STEP 4: Angular 20 -> 21 ==="
ng update @angular/core@21 @angular/cli@21
npm install
ng build
npm test -- --watch=false
git add -A && git commit -m "chore: migrate to Angular 21"
echo "=== Migration complete! ==="
ng version
Handling Breaking Changes
When ng update encounters a breaking change, the process stops with an error
message. Here are the strategies for resolving the most common issues.
Common Breaking Changes and Solutions
| Version | Breaking Change | Solution |
|---|---|---|
| 17→18 | Removal of residual ViewEngine APIs |
Replace with Ivy equivalents |
| 18→19 | standalone: true becomes default |
Add standalone: false to non-standalone components |
| 18→19 | Removal of legacy @angular/http |
Use @angular/common/http |
| 19→20 | Deprecation of Observable-based patterns for some internal services | Migrate to signal-based equivalents |
| 20→21 | TypeScript 5.9 required | Update TypeScript and fix type errors |
| 20→21 | Removal of APIs deprecated since v17 | Consult the Angular deprecation guide |
Resolving Peer Dependency Conflicts
# If ng update fails due to peer dependencies
# Strategy 1: Force the update
ng update @angular/core@21 @angular/cli@21 --force
# Strategy 2: Manually update conflicting dependencies
npm install @angular/core@21 @angular/cli@21 --legacy-peer-deps
npm install
# Strategy 3: Use --allow-dirty if you have uncommitted changes
ng update @angular/core@21 --allow-dirty
# Strategy 4: Clean install from scratch
rm -rf node_modules package-lock.json
npm install
# Verify dependency status
npm ls @angular/core
8. Third-Party Dependency Migration
Third-party dependencies are often the main bottleneck in a migration. Not all libraries immediately support the new Angular version, and some require substantial updates. Here is a guide for the most common libraries.
Main Library Compatibility Matrix
| Library | Angular 18 | Angular 19 | Angular 20 | Angular 21 |
|---|---|---|---|---|
| @angular/material | 18.x | 19.x | 20.x | 21.x |
| @ngrx/store | 18.x | 19.x | 20.x | 21.x |
| @angular/fire | 18.x | 19.x | 18.x+ | 19.x+ |
| ng-bootstrap | 16.x | 17.x | 18.x | 19.x |
| ngx-translate | 15.x | 16.x | 16.x+ | 16.x+ |
| primeng | 17.x | 18.x | 19.x | 19.x+ |
| @ng-icons | 27.x | 28.x | 29.x | 30.x+ |
Dependency Strategy
# 1. List all Angular-related dependencies
npm ls | grep angular
# 2. Check available versions
npm view @angular/material versions --json | tail -20
npm view @ngrx/store versions --json | tail -20
# 3. Update primary dependencies
ng update @angular/material@21
ng update @ngrx/store@21
# 4. For libraries without ng update schematics
npm install ngx-translate@latest
npm install primeng@latest
# 5. Verify compatibility after update
ng build
npm test -- --watch=false
Dependencies Not Yet Updated
If a third-party library does not yet support the target Angular version, you have these options:
- Wait: Monitor the library releases and defer the migration for that dependency
- Temporary fork: If necessary, create a fork with the minimum required changes
- --legacy-peer-deps: Ignore the conflict if the library works anyway (verify with tests)
- Replace: Evaluate compatible alternatives. For example, from
ngx-translateto Angular native i18n - Contribute: Open a PR on the library to add support for the new version
9. Testing After Migration
After each step of the migration, it is essential to verify that the application works correctly. Automated tests are your safety net, but the migration itself can break existing tests. Here is how to approach this phase.
Running the Test Suite
# 1. Run all unit tests
npm test -- --watch=false --browsers=ChromeHeadless
# 2. Run e2e tests (if present)
npm run e2e
# 3. Production build (catches AOT errors)
ng build --configuration=production
# 4. Start the SSR server and verify manually
npm run serve:ssr:portfolio
# 5. Check for deprecation warnings
ng build 2>&1 | grep -i "warning\|deprecated"
# 6. Verify performance with Lighthouse
npx lighthouse http://localhost:4200 --output=json --output-path=./lighthouse-report.json
Common Fixes for Broken Tests
// BEFORE: TestBed with NgModule-style
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [MyComponent], // deprecated with standalone
imports: [HttpClientTestingModule],
providers: [MyService]
}).compileComponents();
});
// AFTER: TestBed with standalone components
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyComponent], // standalone component goes in imports
providers: [
provideHttpClientTesting(), // new provider-based testing
MyService
]
}).compileComponents();
});
// BEFORE: fakeAsync with Zone.js
it('should load data', fakeAsync(() => {
component.loadData();
tick(1000);
expect(component.data).toBeTruthy();
}));
// AFTER: async/await without Zone.js
it('should load data', async () => {
component.loadData();
await fixture.whenStable();
fixture.detectChanges();
expect(component.data()).toBeTruthy(); // signal()
});
Common Test Errors After Migration
| Error | Cause | Solution |
|---|---|---|
NullInjectorError: No provider for HttpClient |
HttpClientTestingModule deprecated | Use provideHttpClientTesting() |
Component is not a known element |
Standalone component not imported | Add the component to imports in TestBed |
Cannot read property of signal |
Signal not invoked as a function | Change component.value to component.value() |
fakeAsync timer not cleared |
Zone.js removed | Convert to async/await |
Expression has changed |
Zoneless change detection | Use fixture.detectChanges() after every change |
10. Final Migration Checklist
To make sure the migration is complete and nothing has been missed, follow this detailed checklist divided into three phases: pre-migration, during migration, and post-migration.
Phase 1: Pre-Migration
- Clean repository: no uncommitted changes
- Dedicated branch created:
feat/angular-21-migration - Green test suite: all tests pass on the current version
- Working production build:
ng buildcompleted without errors - Node.js updated to the minimum required (20.x for Angular 20/21)
- Backup of
package.jsonandpackage-lock.json - Documentation of third-party dependencies and their versions
- Review of breaking changes for each target version on
update.angular.io
Phase 2: During Migration
- Migrate one version at a time: 17→18→19→20→21
- For each step:
ng update,npm install,ng build,npm test - Commit after each successful migration
- Run automatic migration schematics (control flow, standalone, etc.)
- Resolve every deprecation warning before proceeding to the next step
- Update Angular Material and other Angular dependencies to the same major version
- Verify AOT compilation:
ng build --configuration=production - Check the browser console for runtime errors
Phase 3: Post-Migration
- All tests (unit + e2e) pass
- Production build completed without errors or warnings
- SSR working: verify with
npm run serve:ssr - Performance verified: Lighthouse score ≥ 90 across all categories
- Zero deprecation warnings in the build log
- All third-party dependencies compatible and updated
- Zoneless change detection adopted (recommended)
- Modern control flow (@if, @for) replacing structural directives
- Signal inputs/outputs replacing @Input/@Output decorators
- Tests updated for new APIs (provideHttpClientTesting, etc.)
package-lock.jsoncommitted and clean- Deploy to staging environment for manual testing before production
Summary and Series Conclusion
Migrating from Angular 17 to 21 is not just a version update: it is an evolution of your entire application architecture. Moving from the old model based on Zone.js and NgModules to the new Signals-first, standalone-by-default paradigm represents a substantial improvement in performance, maintainability, and developer experience.
The key to success is graduality: migrate one version at a time, test at every step, adopt new APIs progressively. Angular is designed to make this process as painless as possible thanks to automatic schematics and excellent backward compatibility.
Key Concepts of This Article
- Gradual migration: Always one major version at a time (17→18→19→20→21)
- ng update: The primary tool that updates dependencies and applies automatic schematics
- Modern control flow: @if, @for, @switch replace *ngIf, *ngFor, *ngSwitch
- Signal inputs/outputs:
input()andoutput()replace @Input and @Output - Standalone default: From Angular 19 all components are standalone by default
- Stable zoneless: From Angular 20 you can safely remove Zone.js
- TypeScript 5.9: Angular 21 requires TypeScript 5.9 for new features
- Testing: Update tests from NgModule-style to standalone and from fakeAsync to async/await
- Dependencies: Update Angular Material, NgRx and third-party libraries to the same major
Complete Modern Angular Series Summary
| # | Article | Key Concept |
|---|---|---|
| 1 | Angular Signals | Fine-grained reactivity with signal(), computed(), effect() |
| 2 | Zoneless Change Detection | Removing Zone.js for superior performance |
| 3 | Templates @if, @for, @defer | Modern control flow and declarative lazy loading |
| 4 | Standalone Components | NgModule-free, tree-shakable architecture |
| 5 | Signal Forms | Reactive forms based on Signals |
| 6 | SSR and Incremental Hydration | Server-Side Rendering with progressive hydration |
| 7 | Core Web Vitals | Optimizing LCP, INP, CLS for Google ranking |
| 8 | Angular PWA | Progressive Web App with Service Worker |
| 9 | Advanced Dependency Injection | Tree-shakable DI, injection token, multi-provider |
| 10 | Migrating from Angular 17 to 21 | Complete step-by-step migration guide |
With this article, the Modern Angular series comes to a close. We have covered all the fundamental aspects of the framework, from reactive primitives (Signals) to state management (Signal Forms), from rendering strategies (SSR, hydration) to performance optimization (Core Web Vitals, PWA), from architecture (standalone, advanced DI) to practical migration. We hope this series has been useful for mastering Angular in its most modern and performant incarnation.
Related deep dive: 1 - GitHub Actions: CI/CD per App Angular da Zero.
Related deep dive: 2 - Docker per Sviluppatori Frontend: Containerizzare Angular.
Frequently asked questions
What are the key features introduced in each version of Angular from 17 to 21?
The common thread across the last four versions is clear: Signals as the central reactive primitive, removal of Zone.js, and architecture simplification through standalone components.
Why should you never skip versions when migrating an Angular application?
Angular only supports migration one major version at a time. If you are on Angular 17, you must go through 18, then 19, then 20, and finally 21.
What is the purpose of automatic schematics in each step of the migration process?
These are designed to work only between consecutive versions and update your code automatically.
Why is testing after migration essential?
The migration itself can break existing tests, so it's crucial to verify that the application works correctly after each step.
Commenti
Caricamento commenti...
Sign in to comment