Migrez votre application d'Angular 21 vers Angular 22 : prerequis, ng update, OnPush par defaut, Signal Forms stable et injectAsync avec exemples avant et apres.
Prérequis et préparation avant migration
Angular 22 est une version LTS (support long terme) sortie début juin 2026. Comme toute version majeure, elle relève les versions minimales de la chaîne d'outils : les ignorer provoque un échec en plein milieu de la migration. Voici le socle exigé.
| Outil | Angular 21 | Angular 22 (requis) |
|---|---|---|
| Node.js | 20.x / 22.x | 22 LTS minimum |
| TypeScript | 5.8 – 5.9 | 6.0+ (5.9 et antérieurs supprimés) |
| Zone.js | 0.15+ (optionnel) | Optionnel — zoneless par défaut depuis la v21 |
| RxJS | 7.8+ | 7.8+ / 8.x |
Commencez par vérifier votre environnement et l'état de votre dépôt. Une migration ne se fait jamais sur une branche sale.
# Vérifier Node (doit être >= 22) et TypeScript (doit être >= 6.0)
node -v
npx tsc -v
# Vérifier que la version d'Angular est bien la DERNIÈRE 21.x
# (les schematics de v22 supposent un point de départ à jour)
ng version
# Commiter ou stasher tout le travail en cours : git doit être propre
git status
# Créer une branche dédiée à la migration
git checkout -b chore/migration-angular-22
Mettez ensuite à jour la dernière version mineure de la v21 pour partir d'une base saine, puis lancez l'audit de dépendances et la suite de tests de référence :
# S'aligner sur la dernière 21.x avant de sauter en 22
ng update @angular/core@21 @angular/cli@21
# Auditer les paquets qui devront aussi monter de version
npm outdated
# Lancer la suite de tests AVANT migration = point de référence
npm test
Lancer la migration avec ng update
Le cœur de la migration tient en une commande. ng update ne se contente pas de bumper les numéros de version dans package.json : il exécute des schematics, des transformations automatiques de votre code source.
# Mise à jour du framework + CLI vers Angular 22
ng update @angular/core@22 @angular/cli@22
# Si vous utilisez Angular Material / CDK, mettez-les à jour ensemble
ng update @angular/core@22 @angular/cli@22 @angular/material@22
Concrètement, voici ce que la commande réécrit dans un package.json typique — notez la montée obligatoire de TypeScript en 6.x :
// AVANT — package.json (Angular 21)
{
"dependencies": {
"@angular/core": "^21.0.0",
"@angular/common": "^21.0.0",
"@angular/forms": "^21.0.0",
"rxjs": "^7.8.0",
"typescript": "~5.9.0"
}
}
// APRÈS — package.json (Angular 22, réécrit par ng update)
{
"dependencies": {
"@angular/core": "^22.0.0",
"@angular/common": "^22.0.0",
"@angular/forms": "^22.0.0",
"rxjs": "^7.8.0",
"typescript": "~6.0.0"
}
}
Les schematics affichent un journal des fichiers transformés. Gardez-le sous les yeux : il liste exactement quels composants ont été touchés.
✓ Mise à jour de package.json (TypeScript 6.x requis)
✓ Ajout de ChangeDetectionStrategy.Eager sur les composants existants
✓ Migration des imports @angular/forms/signals (Signal Forms stable)
✓ Suppression des références à provideRoutes() / ComponentFactoryResolver
✓ Installation des paquets npm
UPDATE src/app/app.config.ts (438 bytes)
UPDATE src/app/users/user-list.component.ts (1.4 KB)
git diff après la migration et inspectez chaque fichier modifié avant de committer. Un schematic peut occasionnellement laisser un import inutilisé ou une transformation partielle sur du code non standard.
OnPush par défaut : le breaking change majeur
C'est le changement le plus structurant d'Angular 22, et c'est un vrai breaking change. Tout composant sans propriété changeDetection explicite utilise désormais ChangeDetectionStrategy.OnPush par défaut. L'ancien comportement « vérifier à chaque cycle » est conservé sous un nouveau nom : ChangeDetectionStrategy.Eager.
Ce que le schematic fait sur vos composants existants
Pour éviter toute régression silencieuse, la migration ajoute explicitement Eager sur vos composants actuels. Votre application garde donc exactement le même comportement après la mise à jour.
// AVANT — Angular 21 : Default change detection implicite
import { Component } from '@angular/core';
@Component({
selector: 'app-user-card',
template: `<p>{{ name }}</p>`
})
export class UserCardComponent {
name = 'Sarah';
}
// APRÈS — Angular 22 : le schematic préserve le comportement avec Eager
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-user-card',
changeDetection: ChangeDetectionStrategy.Eager, // <-- ajouté par migration
template: `<p>{{ name }}</p>`
})
export class UserCardComponent {
name = 'Sarah';
}
ng generate component n'auront aucune ligne changeDetection et bénéficieront donc d'OnPush par défaut. Les anciens restent en Eager tant que vous ne les migrez pas vous-même.
Migrer un composant vers OnPush
Avec OnPush, Angular ne relance la détection que si une @Input() change de référence, si un événement du template se déclenche, ou si un signal lu dans le template est modifié. Muter un objet en place ne suffit plus.
// AVANT — fonctionne en Eager, CASSÉ si on passe en OnPush
@Component({ selector: 'app-todo', /* ... */ })
export class TodoComponent {
todos = [{ label: 'Acheter du pain' }];
add() {
// Mutation en place : OnPush ne détecte PAS ce changement
this.todos.push({ label: 'Faire le ménage' });
}
}
// APRÈS — compatible OnPush avec un signal (recommandé en v22)
import { Component, signal, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-todo',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@for (t of todos(); track t.label) {
<li>{{ t.label }}</li>
}
`
})
export class TodoComponent {
// Le signal notifie Angular du changement de façon fiable
todos = signal([{ label: 'Acheter du pain' }]);
add() {
// Nouvelle référence de tableau : la détection se déclenche
this.todos.update(list => [...list, { label: 'Faire le ménage' }]);
}
}
Si la migration vers les signals n'est pas immédiate, la transition passe par ChangeDetectorRef.markForCheck() et une nouvelle référence d'objet :
import { Component, ChangeDetectorRef, inject, ChangeDetectionStrategy } from '@angular/core';
@Component({ /* ... */ changeDetection: ChangeDetectionStrategy.OnPush })
export class TodoComponent {
private cdr = inject(ChangeDetectorRef);
todos = [{ label: 'Acheter du pain' }];
add() {
this.todos = [...this.todos, { label: 'Faire le ménage' }];
this.cdr.markForCheck(); // Demande explicite de revérification
}
}
FetchBackend : nouveau backend HTTP par défaut
Angular 22 fait de l'API Fetch le backend par défaut de HttpClient, à la place de XMLHttpRequest. Conséquence directe : withFetch() est déprécié, car il ne sert plus à rien.
// AVANT — Angular 21 : il fallait opter explicitement pour fetch
import { provideHttpClient, withFetch } from '@angular/common/http';
export const appConfig = {
providers: [
provideHttpClient(withFetch()) // withFetch nécessaire
]
};
// APRÈS — Angular 22 : fetch est le défaut, withFetch() inutile
import { provideHttpClient } from '@angular/common/http';
export const appConfig = {
providers: [
provideHttpClient() // fetch par défaut
]
};
// Pour conserver XMLHttpRequest (cas rares : upload progress legacy)
// import { provideHttpClient, withXhr } from '@angular/common/http';
// provideHttpClient(withXhr());
Autre changement lié : l'option monolithique reportProgress est dépréciée au profit de deux options plus précises, reportUploadProgress et reportDownloadProgress.
// AVANT — Angular 21 : une seule option pour tout
this.http.post('/api/upload', formData, {
reportProgress: true,
observe: 'events'
});
// APRÈS — Angular 22 : options dédiées montée / descente
this.http.post('/api/upload', formData, {
reportUploadProgress: true, // progression de l'envoi
reportDownloadProgress: false, // pas besoin de suivre la réponse
observe: 'events'
});
reportUploadProgress. Si vous reposiez sur l'ancien reportProgress: true pour une barre d'upload, vérifiez ce comportement après migration.
Router : paramsInheritanceStrategy et CanMatchFn
Deux changements du Router demandent une attention particulière car ils modifient des comportements existants.
paramsInheritanceStrategy passe à « always »
La valeur par défaut passe de 'emptyOnly' à 'always'. Désormais, les routes enfants héritent toujours des paramètres et des data de leurs routes parentes. Si votre code lit un paramMap en supposant l'ancien comportement, la résolution change.
// AVANT — Angular 21 : héritage limité (emptyOnly par défaut)
provideRouter(routes);
// Une route enfant ne voyait PAS le :orgId du parent sauf si elle n'avait
// elle-même aucun paramètre.
// APRÈS — Angular 22 : héritage systématique (always par défaut)
provideRouter(routes);
// La route enfant hérite TOUJOURS du :orgId parent.
// Pour restaurer l'ancien comportement si nécessaire :
import { provideRouter, withRouterConfig } from '@angular/router';
provideRouter(routes, withRouterConfig({ paramsInheritanceStrategy: 'emptyOnly' }));
CanMatchFn reçoit un troisième paramètre
Les guards CanMatchFn reçoivent maintenant un troisième argument currentSnapshot. Les signatures existantes doivent être ajustées.
// AVANT — Angular 21
export const adminGuard: CanMatchFn = (route, segments) => {
return inject(AuthService).isAdmin();
};
// APRÈS — Angular 22 : nouveau paramètre currentSnapshot disponible
export const adminGuard: CanMatchFn = (route, segments, currentSnapshot) => {
// currentSnapshot donne le contexte de navigation courant
return inject(AuthService).isAdmin();
};
RouterLink gagne un input browserUrl (pour dissocier l'URL affichée de l'URL interne), et withComponentInputBinding() accepte une option unmatchedInputBehavior pour contrôler le binding des inputs non résolus.
Signal Forms et Resource APIs stables
Deux familles d'API expérimentales de la v21 deviennent stables en v22 : les Signal Forms et les Resource APIs (resource(), rxResource(), httpResource()). Le schematic met à jour les chemins d'import.
Signal Forms : import stabilisé
// AVANT — Angular 21 : import expérimental
import { form, Control } from '@angular/forms/signals/experimental';
// APRÈS — Angular 22 : import stable (réécrit par le schematic)
import { form, Control } from '@angular/forms/signals';
Un formulaire complet en approche signal-based : l'état est un signal, la validation déclarative, le template lié directement aux champs via FormField.
import { Component, signal } from '@angular/core';
import { form, Control, required, minLength, email } from '@angular/forms/signals';
@Component({
selector: 'app-signup',
template: `
<form>
<input [control]="profil.name" placeholder="Nom" />
@if (profil.name().errors().length) {
<small class="text-danger">Nom requis (3 caractères min)</small>
}
<input [control]="profil.email" placeholder="Email" />
@if (profil.email().errors().length) {
<small class="text-danger">Email invalide</small>
}
<button [disabled]="!profil().valid()">S'inscrire</button>
</form>
`
})
export class SignupComponent {
private model = signal({ name: '', email: '' });
// form() construit un arbre de champs réactifs avec validation déclarative
profil = form(this.model, (path) => {
required(path.name);
minLength(path.name, 3);
required(path.email);
email(path.email);
});
}
FormGroup / FormControl (Reactive Forms) restent supportés. Signal Forms est une option supplémentaire, pas un remplacement imposé. Un pont SignalFormControl existe dans @angular/forms/signals/compat pour la cohabitation.
Resource APIs : gestion de l'asynchrone dans le graphe de signaux
httpResource() devient stable et gère les lectures réactives sans mélanger HttpClient, RxJS et toSignal(). À réserver aux lectures : le guide officiel déconseille httpResource pour les mutations (POST, PUT, DELETE).
// AVANT — Angular 21 : HttpClient + RxJS + toSignal manuel
import { toSignal } from '@angular/core/rxjs-interop';
readonly userId = signal(1);
readonly user = toSignal(
this.userId$ .pipe(switchMap(id => this.http.get<User>(`/api/users/${id}`)))
);
// APRÈS — Angular 22 : httpResource() stable, réactif au signal
import { httpResource } from '@angular/common/http';
readonly userId = signal(1);
// Se ré-exécute automatiquement quand userId() change
readonly user = httpResource<User>(() => `/api/users/${this.userId()}`);
// user.value(), user.isLoading(), user.error() prêts à l'emploi
@Service() et injectAsync()
Angular 22 stabilise le décorateur @Service() et introduit injectAsync() pour le chargement paresseux de services. Les deux vont de pair : injectAsync() exige un service auto-provisionné (via @Service() ou @Injectable({ providedIn: 'root' })).
Le décorateur @Service()
// AVANT — Angular 21 : déclaration verbeuse
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class AnalyticsService {}
// APRÈS — Angular 22 : @Service() = singleton tree-shakable implicite
import { Service } from '@angular/core';
@Service()
export class AnalyticsService {}
injectAsync() pour le lazy-loading de services
Avant la v22, charger une dépendance lourde à la demande imposait un import dynamique manuel et une gestion d'état explicite. injectAsync() résout tout dans le contexte d'injection, avec code-splitting automatique.
// AVANT — Angular 21 : import dynamique manuel + gestion d'état
import { Component, signal } from '@angular/core';
@Component({ selector: 'app-chart', template: `...` })
export class ChartComponent {
private renderer = signal<ChartRenderer | null>(null);
async ngOnInit() {
const { ChartRenderer } = await import('./heavy-chart-renderer');
this.renderer.set(new ChartRenderer(/* deps résolues à la main */));
}
}
// APRÈS — Angular 22 : injectAsync() résout via le DI Angular
import { Component, injectAsync } from '@angular/core';
@Component({ selector: 'app-chart', template: `...` })
export class ChartComponent {
// Charge le module ET résout ses dépendances (code-splitting auto)
private renderer = injectAsync(() => import('./heavy-chart-renderer')
.then(m => m.ChartRenderer));
async draw() {
const renderer = await this.renderer;
renderer.render();
}
}
injectAsync() supporte des triggers de prefetch comme onIdle() (chargement quand le navigateur est inactif) ou des fonctions PrefetchTrigger personnalisées, pour anticiper le chargement sans bloquer le démarrage.
Hydration incrémentale par défaut
Pour les applications SSR, l'hydration incrémentale devient le comportement par défaut de platform-browser en Angular 22. Le navigateur n'hydrate plus toute la page d'un bloc : il hydrate les zones au fur et à mesure (au scroll, à l'interaction), réduisant drastiquement le JavaScript exécuté au démarrage.
// AVANT — Angular 21 : hydration incrémentale en opt-in explicite
import { provideClientHydration, withIncrementalHydration } from '@angular/platform-browser';
bootstrapApplication(AppComponent, {
providers: [
provideClientHydration(withIncrementalHydration()) // opt-in
]
});
// APRÈS — Angular 22 : incrémentale par défaut, plus besoin du flag
import { provideClientHydration } from '@angular/platform-browser';
bootstrapApplication(AppComponent, {
providers: [
provideClientHydration() // hydration incrémentale par défaut
]
});
Dans les templates, vous pilotez les zones hydratées avec les triggers @defer (hydrate ...) — la zone reste statique côté serveur et ne s'hydrate qu'au déclenchement choisi :
<!-- Le bloc commentaires ne s'hydrate qu'au scroll, pas au chargement -->
@defer (hydrate on viewport) {
<app-comments [postId]="id" />
} @placeholder {
<div class="skeleton">Chargement des commentaires…</div>
}
APIs supprimées et dépréciations
Angular 22 retire plusieurs APIs dépréciées de longue date. Le schematic gère la majorité des cas, mais certaines transformations exigent une relecture humaine. Voici les changements les plus courants.
Création dynamique de composants
ComponentFactoryResolver et ComponentFactory sont supprimés (dépréciés depuis la v13). Utilisez l'API moderne via ViewContainerRef.createComponent().
// AVANT — API supprimée en v22
const factory = this.resolver.resolveComponentFactory(MyComponent);
const ref = this.container.createComponent(factory);
// APRÈS — API moderne (type direct, plus de factory)
const ref = this.container.createComponent(MyComponent);
Modules et routes
// AVANT — createNgModuleRef supprimé
const ref = createNgModuleRef(MyModule, injector);
// APRÈS
const ref = createNgModule(MyModule, injector);
// AVANT — provideRoutes() supprimé
providers: [provideRoutes(routes)]
// APRÈS — utiliser provideRouter()
providers: [provideRouter(routes)]
Bindings et validateurs plus stricts
Plusieurs comportements deviennent des erreurs de compilation, ce qui durcit la sécurité de type :
// AVANT — toléré en v21, ERREUR en v22 :
// 1. les attributs préfixés data- ne lient plus inputs/outputs
<app-item data-label="Bonjour"></app-item> <!-- ne bind plus @Input label -->
// 2. validateurs min/max n'acceptent plus de chaînes
new FormControl(0, [Validators.min('5')]); // ERREUR : passez un number
new FormControl(0, [Validators.min(5)]); // OK
// 3. noms d'input/output/model dupliqués = erreur de compilation
// 4. @for invalides désormais vérifiés au build
Tableau récapitulatif
| Élément | Statut en v22 | Action / remplacement |
|---|---|---|
ComponentFactoryResolver / ComponentFactory | Supprimé | ViewContainerRef.createComponent(Type) |
createNgModuleRef() | Supprimé | createNgModule() |
provideRoutes() | Supprimé | provideRouter() |
ChangeDetectorRef.checkNoChanges() | Retiré de l'API publique | Plus d'usage direct |
| Intégration HammerJS | Supprimée | Gestion de gestes native / lib tierce |
getAngularLib / setAngularLib (@angular/upgrade) | Supprimés | — |
Validateurs min / max en string | Supprimé | Passer un number |
Attributs data- liant inputs/outputs | Supprimé | Binding explicite [input] |
withFetch() / reportProgress | Déprécié | Fetch par défaut / reportUpload/DownloadProgress |
| TypeScript ≤ 5.9 | Non supporté | Monter en 6.0+ |
linkedSignal avec callback set personnalisé, les commentaires HTML dans les balises ouvrantes, et le support expérimental WebMCP pour exposer des outils aux agents IA. Aucun n'est requis pour migrer.
Validation et checklist post-migration
La migration n'est terminée que lorsque l'application compile, les tests passent, et le build de production réussit. Déroulez cette séquence dans l'ordre.
# 1. Vérifier qu'il ne reste pas d'erreurs de compilation TypeScript (6.x)
npm run build
# 2. Relancer toute la suite de tests et comparer au point de référence
npm test
# 3. Builder en mode production (révèle les erreurs AOT/optimizer)
ng build --configuration production
# 4. Démarrer en local et tester les parcours critiques manuellement
ng serve
- Node.js 22 LTS et TypeScript 6.0+ installés et reconnus
- Migration effectuée depuis la dernière version d'Angular 21
-
git diffrelu fichier par fichier après les schematics - Composants critiques : comportement
EagervsOnPushvalidé - Appels HTTP vérifiés sous le backend Fetch (upload progress notamment)
- Routes enfants testées avec
paramsInheritanceStrategy: 'always' - Guards
CanMatchFnmis à jour (3e paramètre) - Aucune référence résiduelle à
provideRoutes()/ComponentFactoryResolver/ HammerJS - Imports Signal Forms et Resource APIs migrés vers le chemin stable
- Dépendances tierces (
@angular/material, NgRx…) montées en version compatible - SSR : hydration incrémentale testée sur les pages à fort trafic
-
ng build --configuration productionréussit sans avertissement bloquant
En résumé, la migration Angular 21 → 22 reste l'une des plus douces de l'histoire récente du framework : ng update automatise l'essentiel, et les breaking changes sont neutralisés par les schematics (ajout de Eager, suppression des APIs retirées). Le vrai travail consiste à relire les diffs, valider les comportements OnPush et Fetch, vérifier l'héritage de paramètres du Router, et adopter progressivement signals, Signal Forms et Resource APIs là où ils apportent le plus de valeur. Migrez par petits commits, et gardez vos tests verts à chaque étape.