Aller au contenu principal

Guide de Migration: Angular v16 vers v21

Guide complet pour migrer les applications Angular de la v16 vers la v21.

Vue d'ensemble

Angular v21 introduit plusieurs changements majeurs:

  • Composants standalone par défaut
  • Nouvelle syntaxe de flux de contrôle (@if, @for, @switch)
  • APIs basées sur les signaux pour la réactivité
  • Guards fonctionnels remplaçant les guards basés sur des classes
  • Nouveau bootstrapping avec bootstrapApplication()

Étapes de Migration

1. Mettre à Jour les Dépendances

# Mettre à jour Angular CLI et Core
ng update @angular/core@21 @angular/cli@21

# Mettre à jour Angular Material (si utilisé)
ng update @angular/material@21

2. Migrer vers les Composants Standalone

Angular v21 utilise les composants standalone par défaut. Exécutez la migration automatique:

ng generate @angular/core:standalone

Cette migration:

  • Convertit les composants en standalone
  • Met à jour les tableaux d'imports
  • Supprime les déclarations NgModule
  • Met à jour la configuration de routage

3. Mettre à Jour la Configuration de Bootstrap

Remplacer le bootstrap NgModule par bootstrapApplication().

Avant (v16):

// main.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';

platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));

Après (v21):

// main.ts
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. Convertir les Guards en Style Fonctionnel

Les guards basés sur des classes sont dépréciés dans la v21. Convertir en guards fonctionnels.

Avant (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;
}
}

Après (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;
};

Appliquer aux routes:

const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivate: [authGuard] // Utiliser le guard fonctionnel
}
];

5. Migrer la Syntaxe de Flux de Contrôle

Angular v21 introduit une nouvelle syntaxe de template remplaçant les directives structurelles.

*ngIf → @if

Avant (v16):

<div *ngIf="user">
Welcome {{ user.name }}
</div>
<div *ngIf="!user">
Please log in
</div>

Après (v21):

@if (user) {
<div>Welcome {{ user.name }}</div>
} @else {
<div>Please log in</div>
}

*ngFor → @for

Avant (v16):

<div *ngFor="let album of albums; let i = index; trackBy: trackByFn">
{{ i + 1 }}. {{ album.name }}
</div>

Après (v21):

@for (album of albums; track album.id) {
<div>{{ $index + 1 }}. {{ album.name }}</div>
}

Variables intégrées:

  • $index - index actuel
  • $first - true si premier élément
  • $last - true si dernier élément
  • $even - true si index pair
  • $odd - true si index impair
  • $count - nombre total d'éléments

[ngSwitch] → @switch

Avant (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>

Après (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. Adopter les APIs Basées sur les Signaux

Angular v21 met l'accent sur les signaux pour la gestion d'état réactive.

Convertir les Observables en Signaux

Avant (v16):

export class AlbumListComponent {
albums$ = this.albumService.getAlbums();

constructor(private albumService: AlbumService) {}
}
<div *ngFor="let album of albums$ | async">
{{ album.name }}
</div>

Après (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>
}

Utiliser les Signaux Calculés

Avant (v16):

export class CartComponent {
cart$ = this.cartService.cart$;

total$ = this.cart$.pipe(
map(items => items.reduce((sum, item) => sum + item.price, 0))
);
}

Après (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)
);
}

Remplacer BehaviorSubject par Signal

Avant (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]);
}
}

Après (v21):

import { signal } from '@angular/core';

export class CartService {
cart = signal<Album[]>([]);

addToCart(album: Album) {
this.cart.update(items => [...items, album]);
}
}

7. Utiliser httpResource pour le Chargement de Données

Angular v21 introduit l'API Resource pour les requêtes HTTP déclaratives.

Avant (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;
}
});
}
}

Après (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. Migrer vers les Formulaires à Signaux

Angular v21 introduit l'API de formulaires basée sur les signaux.

Avant (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>

Après (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. Adopter @ngrx/signals pour la Gestion d'État

Pour un état complexe, utiliser NgRx SignalStore.

Avant (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]);
}
}

Après (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] });
}
}))
);

Liste de Contrôle de Migration

  • Mettre à jour Angular CLI et Core vers v21
  • Exécuter le schéma de migration standalone
  • Mettre à jour main.ts pour utiliser bootstrapApplication()
  • Supprimer app.module.ts et les fichiers NgModule
  • Convertir les guards basés sur des classes en guards fonctionnels
  • Remplacer *ngIf, *ngFor, [ngSwitch] par @if, @for, @switch
  • Convertir les observables en signaux avec toSignal()
  • Remplacer les BehaviorSubjects par signal()
  • Utiliser computed() pour l'état dérivé
  • Adopter httpResource pour les requêtes HTTP
  • Migrer vers les formulaires basés sur les signaux
  • Considérer @ngrx/signals pour un état complexe
  • Mettre à jour les tests pour la nouvelle syntaxe
  • Tester l'application de manière approfondie

Problèmes Courants

Problème: Dépendance Circulaire

Problème: Les composants standalone peuvent créer des dépendances circulaires.

Solution: Extraire les composants partagés dans un fichier séparé ou utiliser des imports dynamiques.

Problème: Configuration des Providers

Problème: Services introuvables après suppression des NgModules.

Solution: S'assurer que les services utilisent providedIn: 'root' ou les ajouter au tableau providers dans bootstrapApplication().

Problème: Chargement Différé

Problème: Les routes de chargement différé ne fonctionnent pas après la migration.

Solution: Mettre à jour la configuration des routes:

// Avant
{ path: 'admin', loadChildren: () => import('./admin/admin.module') }

// Après
{ path: 'admin', loadComponent: () => import('./admin/admin.component') }

Problème: FormsModule/ReactiveFormsModule

Problème: Les formulaires ne fonctionnent pas après suppression des NgModules.

Solution: Importer dans le composant:

import { ReactiveFormsModule } from '@angular/forms';

@Component({
imports: [ReactiveFormsModule],
// ...
})

Bonnes Pratiques

  1. Migration Graduelle: Migrer une fonctionnalité à la fois
  2. Couverture des Tests: S'assurer d'une bonne couverture de tests avant la migration
  3. Utiliser les Schematics: Laisser Angular CLI gérer les migrations répétitives
  4. Adopter les Signaux: Embrasser les APIs basées sur les signaux pour de meilleures performances
  5. Simplifier l'État: Utiliser les signaux pour réduire la complexité RxJS
  6. Sûreté des Types: Tirer parti de TypeScript avec les signaux
  7. Suivre le Guide Officiel: Consulter le Guide de Mise à Jour Angular

Ressources