Migration Guide: Angular v16 to v21
Comprehensive guide for migrating Angular applications from v16 to v21.
Overview
Section titled “Overview”Angular v21 introduces several major changes:
- Standalone components as the default
- New control flow syntax (@if, @for, @switch)
- Signal-based APIs for reactivity
- Functional guards replacing class-based guards
- New bootstrapping with
bootstrapApplication()
Migration Steps
Section titled “Migration Steps”1. Update Dependencies
Section titled “1. Update Dependencies”# Update Angular CLI and Coreng update @angular/core@21 @angular/cli@21
# Update Angular Material (if used)ng update @angular/material@212. Migrate to Standalone Components
Section titled “2. Migrate to Standalone Components”Angular v21 uses standalone components by default. Run the automatic migration:
ng generate @angular/core:standaloneThis migration:
- Converts components to standalone
- Updates imports arrays
- Removes NgModule declarations
- Updates routing configuration
3. Update Bootstrap Configuration
Section titled “3. Update Bootstrap Configuration”Replace the NgModule bootstrap with bootstrapApplication().
Before (v16):
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';import { AppModule } from './app/app.module';
platformBrowserDynamic() .bootstrapModule(AppModule) .catch(err => console.error(err));After (v21):
import { bootstrapApplication } from '@angular/platform-browser';import { provideRouter } from '@angular/router';import { provideHttpClient } from '@angular/common/http';import { provideAnimations } from '@angular/platform-browser/animations';import { AppComponent } from './app/app.component';import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, { providers: [ provideRouter(routes), provideHttpClient(), provideAnimations() ]});4. Convert Guards to Functional Style
Section titled “4. Convert Guards to Functional Style”Class-based guards are deprecated in v21. Convert to functional guards.
Before (v16):
import { Injectable } from '@angular/core';import { CanActivate, Router } from '@angular/router';
@Injectable({ providedIn: 'root' })export class AuthGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) {}
canActivate(): boolean { if (this.authService.isLoggedIn()) { return true; } this.router.navigate(['/login']); return false; }}After (v21):
import { inject } from '@angular/core';import { CanActivateFn, Router } from '@angular/router';
export const authGuard: CanActivateFn = (route, state) => { const authService = inject(AuthService); const router = inject(Router);
if (authService.isLoggedIn()) { return true; } router.navigate(['/login']); return false;};Apply to routes:
const routes: Routes = [ { path: 'admin', component: AdminComponent, canActivate: [authGuard] // Use functional guard }];5. Migrate Control Flow Syntax
Section titled “5. Migrate Control Flow Syntax”Angular v21 introduces new template syntax replacing structural directives.
*ngIf → @if
Section titled “*ngIf → @if”Before (v16):
<div *ngIf="user"> Welcome {{ user.name }}</div><div *ngIf="!user"> Please log in</div>After (v21):
@if (user) { <div>Welcome {{ user.name }}</div>} @else { <div>Please log in</div>}*ngFor → @for
Section titled “*ngFor → @for”Before (v16):
<div *ngFor="let album of albums; let i = index; trackBy: trackByFn"> {{ i + 1 }}. {{ album.name }}</div>After (v21):
@for (album of albums; track album.id) { <div>{{ $index + 1 }}. {{ album.name }}</div>}Built-in variables:
$index- current index$first- true if first item$last- true if last item$even- true if even index$odd- true if odd index$count- total number of items
[ngSwitch] → @switch
Section titled “[ngSwitch] → @switch”Before (v16):
<div [ngSwitch]="status"> <p *ngSwitchCase="'loading'">Loading...</p> <p *ngSwitchCase="'error'">Error occurred</p> <p *ngSwitchCase="'success'">Success!</p> <p *ngSwitchDefault>Unknown status</p></div>After (v21):
@switch (status) { @case ('loading') { <p>Loading...</p> } @case ('error') { <p>Error occurred</p> } @case ('success') { <p>Success!</p> } @default { <p>Unknown status</p> }}6. Adopt Signal-Based APIs
Section titled “6. Adopt Signal-Based APIs”Angular v21 emphasizes signals for reactive state management.
Convert Observables to Signals
Section titled “Convert Observables to Signals”Before (v16):
export class AlbumListComponent { albums$ = this.albumService.getAlbums();
constructor(private albumService: AlbumService) {}}<div *ngFor="let album of albums$ | async"> {{ album.name }}</div>After (v21):
import { toSignal } from '@angular/core/rxjs-interop';
export class AlbumListComponent { albums = toSignal(this.albumService.getAlbums(), { initialValue: [] });
constructor(private albumService: AlbumService) {}}@for (album of albums(); track album.id) { <div>{{ album.name }}</div>}Use Computed Signals
Section titled “Use Computed Signals”Before (v16):
export class CartComponent { cart$ = this.cartService.cart$;
total$ = this.cart$.pipe( map(items => items.reduce((sum, item) => sum + item.price, 0)) );}After (v21):
import { computed } from '@angular/core';
export class CartComponent { cart = this.cartService.cart; // signal
total = computed(() => this.cart().reduce((sum, item) => sum + item.price, 0) );}Replace BehaviorSubject with Signal
Section titled “Replace BehaviorSubject with Signal”Before (v16):
export class CartService { private cartSubject = new BehaviorSubject<Album[]>([]); cart$ = this.cartSubject.asObservable();
addToCart(album: Album) { const current = this.cartSubject.value; this.cartSubject.next([...current, album]); }}After (v21):
import { signal } from '@angular/core';
export class CartService { cart = signal<Album[]>([]);
addToCart(album: Album) { this.cart.update(items => [...items, album]); }}7. Use httpResource for Data Fetching
Section titled “7. Use httpResource for Data Fetching”Angular v21 introduces the Resource API for declarative HTTP requests.
Before (v16):
export class AlbumListComponent implements OnInit { albums: Album[] = []; loading = false; error: string | null = null;
ngOnInit() { this.loading = true; this.albumService.getAlbums().subscribe({ next: (albums) => { this.albums = albums; this.loading = false; }, error: (err) => { this.error = err.message; this.loading = false; } }); }}After (v21):
import { httpResource } from '@angular/core';
export class AlbumListComponent { albumsResource = httpResource({ request: () => ({ url: '/api/albums' }), loader: () => this.http.get<Album[]>('/api/albums') });
albums = computed(() => this.albumsResource.value() ?? []); loading = computed(() => this.albumsResource.isLoading()); error = computed(() => this.albumsResource.error());}8. Migrate to Signal Forms
Section titled “8. Migrate to Signal Forms”Angular v21 introduces signal-based forms API.
Before (v16):
import { FormBuilder, Validators } from '@angular/forms';
export class AlbumFormComponent { albumForm = this.fb.group({ name: ['', Validators.required], artist: ['', Validators.required], price: [0, [Validators.required, Validators.min(0)]] });
constructor(private fb: FormBuilder) {}}<form [formGroup]="albumForm"> <input formControlName="name"> <input formControlName="artist"> <input formControlName="price" type="number"></form>After (v21):
import { form, Field, required, min } from '@angular/forms/signals';
export class AlbumFormComponent { albumForm = form( signal<Album>({ id: 0, name: '', artist: '', price: 0 }), (path) => { required(path.name); required(path.artist); required(path.price); min(path.price, 0); } );}<form> <input [field]="albumForm.path.name"> <input [field]="albumForm.path.artist"> <input [field]="albumForm.path.price" type="number"></form>9. Adopt @ngrx/signals for State Management
Section titled “9. Adopt @ngrx/signals for State Management”For complex state, use NgRx SignalStore.
Before (v16):
import { BehaviorSubject } from 'rxjs';
export class AlbumStore { private albumsSubject = new BehaviorSubject<Album[]>([]); albums$ = this.albumsSubject.asObservable();
addAlbum(album: Album) { this.albumsSubject.next([...this.albumsSubject.value, album]); }}After (v21):
import { signalStore, withState, withMethods, withComputed } from '@ngrx/signals';
export const AlbumStore = signalStore( { providedIn: 'root' }, withState({ albums: [] as Album[] }), withComputed(({ albums }) => ({ albumCount: computed(() => albums().length) })), withMethods((store) => ({ addAlbum(album: Album) { patchState(store, { albums: [...store.albums(), album] }); } })));Migration Checklist
Section titled “Migration Checklist”- Update Angular CLI and Core to v21
- Run standalone migration schematic
- Update
main.tsto usebootstrapApplication() - Remove
app.module.tsand NgModule files - Convert class-based guards to functional guards
- Replace
*ngIf,*ngFor,[ngSwitch]with@if,@for,@switch - Convert observables to signals with
toSignal() - Replace BehaviorSubjects with
signal() - Use
computed()for derived state - Adopt
httpResourcefor HTTP requests - Migrate to signal-based forms
- Consider @ngrx/signals for complex state
- Update tests for new syntax
- Test application thoroughly
Common Issues
Section titled “Common Issues”Issue: Circular Dependency
Section titled “Issue: Circular Dependency”Problem: Standalone components can create circular dependencies.
Solution: Extract shared components to a separate file or use dynamic imports.
Issue: Provider Configuration
Section titled “Issue: Provider Configuration”Problem: Services not found after removing NgModules.
Solution: Ensure services use providedIn: 'root' or add to providers array in bootstrapApplication().
Issue: Lazy Loading
Section titled “Issue: Lazy Loading”Problem: Lazy loading routes don’t work after migration.
Solution: Update route configuration:
// Before{ path: 'admin', loadChildren: () => import('./admin/admin.module') }
// After{ path: 'admin', loadComponent: () => import('./admin/admin.component') }Issue: FormsModule/ReactiveFormsModule
Section titled “Issue: FormsModule/ReactiveFormsModule”Problem: Forms don’t work after removing NgModules.
Solution: Import in component:
import { ReactiveFormsModule } from '@angular/forms';
@Component({ imports: [ReactiveFormsModule], // ...})Best Practices
Section titled “Best Practices”- Gradual Migration: Migrate one feature at a time
- Test Coverage: Ensure good test coverage before migrating
- Use Schematics: Let Angular CLI handle repetitive migrations
- Adopt Signals: Embrace signal-based APIs for better performance
- Simplify State: Use signals to reduce RxJS complexity
- Type Safety: Leverage TypeScript with signals
- Follow Official Guide: Check Angular Update Guide

