Migration vers Signal Forms dans Angular 22 : guide pratique
Angular 22, sorti en juin 2026, marque l'entrée officielle dans l'ère Signal-First. Les Signal Forms passent stable, le mode Zoneless est officiellement recommandé, et les Selectorless Components simplifient la composition de templates. C'est l'une des releases les plus structurantes d'Angular depuis l'introduction des Signals en v16.
Dans cet article, je me concentre sur les Signal Forms : pourquoi c'est important, ce que ça change concrètement, et comment aborder la migration depuis vos Reactive Forms existants.
Pourquoi les Signal Forms changent la donne
Le problème avec Reactive Forms
Avec le modèle ReactiveFormsModule, chaque modification d'un champ déclenche une réévaluation de l'arbre complet de FormGroup. Angular parcourt la hiérarchie du formulaire, recalcule les états de validité, les statuts dirty/pristine/touched, et notifie tous les abonnés valueChanges et statusChanges.
Sur un formulaire simple, c'est invisible. Sur un formulaire métier de 30 à 50 champs avec des validators croisés et asynchrones, cela génère des cycles de détection inutiles — surtout si vous êtes encore sur Zone.js.
La promesse des Signal Forms
Les Signal Forms reposent sur la réactivité fine-grained (granulaire) : seul le Signal correspondant au champ modifié est mis à jour, et uniquement les parties du template qui dépendent de ce Signal sont re-rendues.
Concrètement :
- Vous modifiez le champ
email→ seul le composant lié àemailest mis à jour. - Les 49 autres champs ne bougent pas.
- Couplé au mode Zoneless (pas de Zone.js pour intercepter les events), les cycles de détection deviennent entièrement contrôlés par vous.
La nouvelle API Signal Forms
L'architecture est fondamentalement différente des Reactive Forms. Le modèle est un signal() Angular standard, et form() (importé depuis @angular/forms/signals) crée un field tree — un objet qui reflète la structure du modèle et permet d'accéder à l'état de chaque champ.
Avant (Reactive Forms)
import { Component } from "@angular/core";
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
@Component({
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="email" type="email" placeholder="Email" />
@if (form.get('email')?.touched && form.get('email')?.invalid) {
<span class="error">Email requis</span>
}
<input formControlName="password" type="password" placeholder="Mot de passe" />
<button type="submit" [disabled]="form.invalid">Se connecter</button>
</form>
`,
})
export class LoginComponent {
form: FormGroup;
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
});
}
}
Après (Signal Forms — Angular 22)
import { Component, signal } from "@angular/core";
import { form, FormField, required, email, minLength } from "@angular/forms/signals";
interface LoginData {
email: string;
password: string;
}
@Component({
imports: [FormField],
template: `
<form (submit)="onSubmit()">
<input [formField]="loginForm.email" type="email" placeholder="Email" />
@if (loginForm.email().touched() && loginForm.email().invalid()) {
@for (error of loginForm.email().errors(); track error) {
<span class="error">{{ error.message }}</span>
}
}
<input [formField]="loginForm.password" type="password" placeholder="Mot de passe" />
<button type="submit" [disabled]="loginForm().invalid()">Se connecter</button>
</form>
`,
})
export class LoginComponent {
loginModel = signal<LoginData>({ email: "", password: "" });
loginForm = form(this.loginModel, (p) => {
required(p.email, { message: "Email requis" });
email(p.email, { message: "Format invalide" });
required(p.password, { message: "Mot de passe requis" });
minLength(p.password, 8, { message: "Minimum 8 caractères" });
});
}
Points clés :
- Le modèle est un
signal()standard — c'est la source de vérité. form(model, schemaFn)crée le field tree. La schema function (2ᵉ argument) centralise toutes les règles de validation.- La directive
[formField]remplace[formControl]. Elle est importée depuis@angular/forms/signals. - Dans le template,
loginForm.email()appelle le champ comme une fonction pour obtenir sonFieldState, qui expose des signals :.value(),.errors(),.touched(),.invalid(), etc.
La validation : une schema function centralisée
Toutes les règles de validation sont déclarées dans la schema function passée en 2ᵉ argument de form(). Les helpers (required, email, minLength, max, pattern…) sont importés depuis @angular/forms/signals.
import { required, email, minLength, validate } from "@angular/forms/signals";
registrationForm = form(this.registrationModel, (p) => {
required(p.username, { message: "Nom d'utilisateur requis" });
minLength(p.username, 3, { message: "Minimum 3 caractères" });
required(p.email, { message: "Email requis" });
email(p.email, { message: "Format invalide" });
// Validator custom avec validate()
validate(p.confirmPassword, ({ value, valueOf }) => {
if (value() !== valueOf(p.password)) {
return { kind: "passwordMismatch", message: "Les mots de passe ne correspondent pas" };
}
return null;
});
});
Pour la validation asynchrone (vérifier l'unicité d'un username côté serveur, par exemple), Signal Forms expose validateHttp() :
import { validateHttp } from "@angular/forms/signals";
usernameForm = form(this.usernameModel, (p) => {
required(p.username);
validateHttp(p.username, {
request: ({ value }) => `/api/check-username?username=${value()}`,
onSuccess: (response: { taken: boolean }) => {
if (response.taken) {
return { kind: "usernameTaken", message: "Ce nom est déjà pris" };
}
return null;
},
onError: () => ({ kind: "networkError", message: "Impossible de vérifier la disponibilité" }),
});
});
Le signal pending() indique que la validation async est en cours :
@if (usernameForm.username().pending()) {
<span class="hint">Vérification en cours…</span>
}
Les pièges à connaître avant de migrer
1. Pas de schematic automatique
Il n'existe pas de ng update qui convertit vos Reactive Forms en Signal Forms. La migration est manuelle. Prévoyez du temps, surtout pour les formulaires avec des validators croisés ou des arrays dynamiques.
2. La validation des arrays avec applyEach()
Les arrays se déclarent directement dans le modèle signal. Pour valider chaque item, on utilise applyEach() dans la schema function :
import { applyEach, required, min } from "@angular/forms/signals";
orderForm = form(this.orderModel, (p) => {
required(p.customerName);
applyEach(p.items, (item) => {
required(item.name, { message: "Nom de l'article requis" });
min(item.quantity, 1, { message: "Quantité minimum : 1" });
});
});
3. La migration peut être progressive grâce à compatForm
Vous n'avez pas à tout réécrire d'un coup. Signal Forms propose deux stratégies de coexistence :
Top-down — compatForm (importé depuis @angular/forms/signals/compat) permet d'intégrer des FormControl existants à l'intérieur d'un Signal Form. Utile pour conserver des validators complexes en RxJS le temps de les réécrire :
import { signal } from "@angular/core";
import { FormControl, Validators } from "@angular/forms";
import { compatForm } from "@angular/forms/signals/compat";
readonly passwordControl = new FormControl("", [Validators.required, myEnterpriseValidator()]);
readonly user = signal({ email: "", password: this.passwordControl });
// Le field tree proxifie automatiquement le FormControl
readonly f = compatForm(this.user);
Bottom-up — SignalFormControl expose un Signal Form comme un FormControl standard, pour l'intégrer dans un FormGroup existant :
import { FormGroup } from "@angular/forms";
import { SignalFormControl } from "@angular/forms/signals/compat";
import { required } from "@angular/forms/signals";
emailControl = new SignalFormControl("", (p) => {
required(p, { message: "Email requis" });
});
form = new FormGroup({ email: this.emailControl });
Stratégie de migration recommandée
Approche progressive, formulaire par formulaire :
- Commencer par les formulaires simples (login, recherche) pour se familiariser avec l'API.
- Utiliser
compatFormpour les formulaires complexes avec des validators RxJS difficiles à réécrire immédiatement. - Combiner avec le mode Zoneless (
provideZonelessChangeDetection()) — les deux features sont complémentaires et le gain perf est maximal ensemble. - Mesurer avec Angular DevTools avant/après.
Checklist de migration par formulaire :
- Remplacer
FormBuilderparsignal()+form() - Migrer les validators dans la schema function (
required,email,minLength… depuis@angular/forms/signals) - Remplacer
[formControl]par[formField]dans les templates - Mettre à jour les accès aux erreurs :
field().errors(),field().touched(),field().invalid() - Supprimer
ReactiveFormsModulesi le composant n'en a plus besoin
Angular 22 : un LTS candidate, le bon moment pour migrer
Angular 22 est proposé comme Long-Term Support candidate, ce qui signifie un support étendu pour les équipes qui l'adoptent. Les Signal Forms ne sont pas une simple nouveauté syntaxique — ils représentent un changement de paradigme dans la façon dont Angular gère la réactivité. Combinés au mode Zoneless et aux Selectorless Components (aussi stables en v22), ils constituent le socle de l'Angular moderne.
La migration demande un investissement, mais les gains en performance et en lisibilité du code en valent la peine — surtout sur les applications métier à formulaires complexes.