Front-end angularforall.com

- Migration Angular 21 vers 22 : guide pas à pas

Angular Angular-22 Angular-21 Migration Ng-Update Signal-Forms Inject-Async Onpush Breaking-Changes Hydration-Incrementale Zoneless Typescript Schematics Front-End
Migration Angular 21 vers 22 : guide pas à pas

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.js20.x / 22.x22 LTS minimum
TypeScript5.8 – 5.96.0+ (5.9 et antérieurs supprimés)
Zone.js0.15+ (optionnel)Optionnel — zoneless par défaut depuis la v21
RxJS7.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
Règle d'or : migrez toujours une seule version majeure à la fois. Si vous êtes en Angular 20, faites d'abord 20 → 21, validez, puis 21 → 22. Sauter une version casse l'enchaînement des schematics automatiques.

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
Note : conservez la sortie des tests « avant migration ». Après la mise à jour, vous comparerez ligne à ligne pour distinguer une vraie régression d'un test déjà cassé en amont.

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)
Toujours relire le diff : les schematics sont fiables à 95 %. Lancez 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';
}
À retenir : les nouveaux composants générés via 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'
});
Attention : avec le backend Fetch, le suivi de progression d'upload nécessite explicitement 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();
};
Bon à savoir : 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);
    });
}
Pas de migration forcée : vos 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();
    }
}
Préchargement : 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>
}
Gain mesuré : sur une page avec sections lourdes (commentaires, recommandations, widgets), l'hydration incrémentale réduit typiquement le JavaScript exécuté au démarrage de 40 à 70 %, améliorant directement le score INP de Core Web Vitals. Les Resource APIs profitent aussi d'un meilleur cache côté SSR.

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 / ComponentFactorySuppriméViewContainerRef.createComponent(Type)
createNgModuleRef()SupprimécreateNgModule()
provideRoutes()SuppriméprovideRouter()
ChangeDetectorRef.checkNoChanges()Retiré de l'API publiquePlus d'usage direct
Intégration HammerJSSuppriméeGestion de gestes native / lib tierce
getAngularLib / setAngularLib (@angular/upgrade)Supprimés
Validateurs min / max en stringSuppriméPasser un number
Attributs data- liant inputs/outputsSuppriméBinding explicite [input]
withFetch() / reportProgressDépréciéFetch par défaut / reportUpload/DownloadProgress
TypeScript ≤ 5.9Non supportéMonter en 6.0+
Au-delà de la migration : Angular 22 apporte aussi des nouveautés non bloquantes à explorer ensuite — les composants selectorless (import direct sans sélecteur), 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 diff relu fichier par fichier après les schematics
  • Composants critiques : comportement Eager vs OnPush validé
  • Appels HTTP vérifiés sous le backend Fetch (upload progress notamment)
  • Routes enfants testées avec paramsInheritanceStrategy: 'always'
  • Guards CanMatchFn mis à 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 production réussit sans avertissement bloquant
Conseil final : déployez d'abord sur un environnement de staging et laissez tourner 24 à 48 h sous trafic réel avant la production. Les régressions de change detection (OnPush) et d'hydration se révèlent souvent sous charge, pas dans les tests unitaires.

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.

Partager