11 min de lecture - Signals Angular en 2026 : guide de migration pour l'entreprise
Angular Development
Date de publication 20 février 2026 · Auteur Exceev Consulting
Les signals sont des primitives de réactivité stables dans Angular. La détection de changement sans Zone.js est également stable, mais il s'agit d'un choix architectural distinct : les signals aident Angular à savoir quand l'état change, tandis que le mode zoneless modifie la planification de la détection de changement. Une grande application peut adopter l'un ou l'autre progressivement.
Ce guide s’adresse aux équipes qui gèrent des applications Angular comptant plus de 100 composants, de multiples feature modules et de vraies contraintes de déploiement. Il couvre ce que les signals remplacent, comment migrer de façon incrémentale, les nouveaux patterns à maîtriser et les gains de performance à attendre.
Les signals ne remplacent pas Zone.js à eux seuls. Ils suivent les dépendances d'état et signalent à Angular les changements de valeurs lues par un template. Si l'application adopte aussi le mode zoneless, Angular documente les mécanismes de notification et les travaux de compatibilité requis. Migrez un périmètre limité, testez le comportement et l'accessibilité, puis comparez les traces et les performances perçues à la version initiale.
Les responsabilités reprises par les signals
Comprendre ce que les signals remplacent est la première étape d’un plan de migration concret.
Zone.js et la détection de changement automatique
Zone.js patche chaque API asynchrone du navigateur, setTimeout, Promise, addEventListener, XHR, afin qu’Angular puisse détecter qu’un changement est potentiellement survenu et déclencher la détection de changement sur l’ensemble de l’arbre de composants. C’est puissant, mais coûteux : chaque événement asynchrone déclenche une vérification complète de l’arbre, même si rien de pertinent n’a changé.
Les signals remplacent ce mécanisme par une réactivité explicite. Lorsque la valeur d’un signal change, Angular sait exactement quels composants et templates en dépendent, et ne met à jour que ceux-là. Plus de patching, plus de parcours complet de l’arbre, plus de cycles gaspillés.
Les patterns RxJS simples
Tous les usages de RxJS n’ont pas vocation à migrer. Les signals remplacent les patterns où RxJS servait de conteneur d’état plutôt que de véritable traitement de flux :
- BehaviorSubject pour l’état d’un composant, remplacé par
signal() - combineLatest pour l’état dérivé, remplacé par
computed() - switchMap simple pour le chargement de données, remplacé par
resource() - Subject pour la communication d’événements entre parent et enfant, remplacé par les signal inputs et les model signals
Ce que RxJS fait encore mieux
RxJS reste l’outil approprié pour :
- L’orchestration asynchrone complexe (retry, debounce, throttle, conditions de concurrence)
- Les flux WebSocket et les données temps réel
- Les flux d’événements nécessitant une gestion de la contre-pression (backpressure)
- L’interopérabilité avec les bibliothèques qui émettent des observables
La migration ne consiste pas à « supprimer tout RxJS ». Elle consiste à « arrêter d’utiliser RxJS comme outil de gestion d’état, et l’utiliser là où les flux sont la bonne abstraction ».
Stratégie de migration étape par étape pour les bases de code d’entreprise
Réécrire une grande application Angular pour adopter les signals n’est ni réaliste ni nécessaire. Le framework a été conçu pour permettre une adoption incrémentale. Voici le chemin de migration qui fonctionne pour les équipes d’entreprise.
Phase 1 : convertir les composants feuilles (semaines 1 à 4)
Commencez par les composants sans enfants, ou dont les enfants sont purement présentationnels. Ce sont les cibles les moins risquées.
Pour chaque composant feuille :
- Remplacez les décorateurs
@Input()par des signal inputsinput() - Remplacez les variables d’état local du composant par des appels à
signal() - Remplacez les getters utilisés dans les templates par des signals
computed() - Mettez à jour le template pour appeler les signals comme des fonctions :
{{ name() }}au lieu de{{ name }}
// Before
@Component({ ... })
export class MetricCardComponent {
@Input() title: string = '';
@Input() value: number = 0;
@Input() trend: 'up' | 'down' | 'flat' = 'flat';
get formattedValue(): string {
return this.value.toLocaleString();
}
get trendIcon(): string {
return this.trend === 'up' ? 'arrow_upward' : this.trend === 'down' ? 'arrow_downward' : 'remove';
}
}
// After
@Component({ ... })
export class MetricCardComponent {
title = input<string>('');
value = input<number>(0);
trend = input<'up' | 'down' | 'flat'>('flat');
formattedValue = computed(() => this.value().toLocaleString());
trendIcon = computed(() =>
this.trend() === 'up' ? 'arrow_upward' : this.trend() === 'down' ? 'arrow_downward' : 'remove'
);
}
Cette phase présente un risque faible, car les composants feuilles sont isolés. Exécutez vos tests existants après chaque conversion. Si les tests passent, la migration est correcte.
Phase 2 : remplacer les BehaviorSubject simples (semaines 5 à 8)
Les services qui utilisent BehaviorSubject comme conteneurs d’état constituent la cible suivante. Ils sont fréquents dans les bases de code Angular d’entreprise et sont souvent à l’origine de la complexité de gestion des souscriptions.
// Before
@Injectable({ providedIn: 'root' })
export class UserPreferencesService {
private _theme = new BehaviorSubject<'light' | 'dark'>('light')
theme$ = this._theme.asObservable()
setTheme(theme: 'light' | 'dark') {
this._theme.next(theme)
}
}
// After
@Injectable({ providedIn: 'root' })
export class UserPreferencesService {
theme = signal<'light' | 'dark'>('light')
setTheme(theme: 'light' | 'dark') {
this.theme.set(theme)
}
}
Les composants qui souscrivaient auparavant à theme$ et géraient le nettoyage de la souscription se contentent désormais de lire this.userPrefs.theme() dans leurs templates ou leurs signals computed. Plus de souscription, plus de takeUntilDestroyed, plus de pipe async, le signal est lu directement.
Phase 3 : adopter linkedSignal pour l’état dérivé (semaines 9 à 12)
linkedSignal est un pattern pour un état dont la valeur initiale dérive d’un autre signal, mais qui peut ensuite être modifié de façon indépendante. Il remplace un pattern courant en entreprise consistant à charger une valeur par défaut depuis un service, puis à laisser l’utilisateur la surcharger.
// A filter panel that defaults to the user's saved preferences
// but allows local overrides
export class FilterPanelComponent {
private userPrefs = inject(UserPreferencesService)
// linkedSignal: initial value comes from userPrefs, but local changes are independent
selectedRegion = linkedSignal(() => this.userPrefs.defaultRegion())
selectedDateRange = linkedSignal(() => this.userPrefs.defaultDateRange())
// Local overrides do not write back to preferences
onRegionChange(region: string) {
this.selectedRegion.set(region)
}
// Reset to defaults
resetFilters() {
// When the source signal changes, linkedSignal re-derives automatically
// Or you can manually trigger by resetting the source
}
}
Cela élimine le code de synchronisation manuel que les applications d’entreprise ont tendance à accumuler : chargement des valeurs par défaut, suivi des surcharges locales, réinitialisation aux valeurs par défaut, et gestion des conditions de concurrence entre état distant et état local.
Phase 4 : adopter resource() pour le chargement de données asynchrone (semaines 13 à 16)
L’API resource() offre une approche basée sur les signals pour gérer le chargement de données asynchrone, qui remplace le pattern courant consistant à déclencher des appels HTTP dans ngOnInit ou via des chaînes switchMap RxJS.
export class DashboardComponent {
private analyticsService = inject(AnalyticsService)
selectedPeriod = signal<'week' | 'month' | 'quarter'>('month')
dashboardData = resource({
request: () => this.selectedPeriod(),
loader: ({ request: period }) => this.analyticsService.loadDashboard(period),
})
// In template:
// @if (dashboardData.isLoading()) { <spinner /> }
// @if (dashboardData.value(); as data) { <dashboard [data]="data" /> }
// @if (dashboardData.error(); as err) { <error-message [error]="err" /> }
}
Le pattern resource() gère les états de chargement, les états d’erreur et le rechargement automatique lorsque le signal de requête change. Il remplace une quantité significative de code répétitif que les équipes d’entreprise géraient jusque-là avec des services d’état de chargement maison ou des effects NgRx.
Activer la détection de changement zoneless
Le mode zoneless est l’aboutissement de la migration vers les signals. Une fois que vos composants utilisent des signals pour leur état réactif, vous pouvez supprimer entièrement Zone.js, ce qui élimine le coût du monkey-patching des API du navigateur et de l’exécution de cycles complets de détection de changement.
Adoption progressive du mode zoneless
Il n’est pas nécessaire de basculer toute l’application en zoneless d’un coup. Angular prend en charge un mode hybride dans lequel les composants basés sur les signals se désengagent de la détection de changement de Zone.js, tandis que les composants existants continuent de l’utiliser.
Commencez par activer la détection zoneless dans la configuration de votre application :
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }), // keep Zone for now
// When ready for full zoneless:
// provideExperimentalZonelessChangeDetection(),
],
}
Pour les équipes d’entreprise, l’approche recommandée est la suivante :
- Migrer les composants vers les signals (phases 1 à 4 ci-dessus)
- Activer la détection de changement
OnPushsur les composants migrés - Tester en profondeur dans ce mode hybride
- Basculer vers
provideExperimentalZonelessChangeDetection()lorsque la couverture est suffisante - Retirer Zone.js des polyfills pour réduire la taille du bundle
Ce qui casse sans Zone.js
Lorsque vous retirez Zone.js, tout composant qui repose sur la détection de changement automatique déclenchée par des opérations asynchrones cesse de se mettre à jour. Voici les patterns courants qui cassent :
- La mutation directe de propriétés dans des callbacks
setTimeoutouPromise.then - Les bindings de template vers des propriétés d’objet simples qui changent de façon asynchrone
- Les bibliothèques tierces qui modifient l’état du composant à l’insu d’Angular
Ces patterns doivent être convertis en mises à jour de signals, ou encapsulés dans des appels explicites à ChangeDetectorRef.markForCheck(), avant de passer en mode zoneless complet.
Stratégie de test pour les composants basés sur les signals
La migration vers les signals ne devrait pas nécessiter de réécrire votre suite de tests. La stratégie de test consiste à adapter les tests existants de façon incrémentale.
Tests unitaires des composants basés sur les signals
Les composants basés sur les signals sont plus faciles à tester, car l’état y est explicite. Plutôt que de déclencher la détection de changement puis de vérifier le rendu du template, vous pouvez tester directement les valeurs des signals.
describe('MetricCardComponent', () => {
it('should compute formatted value', () => {
const fixture = TestBed.createComponent(MetricCardComponent)
const component = fixture.componentInstance
// Set signal input using componentRef
fixture.componentRef.setInput('value', 1234567)
expect(component.formattedValue()).toBe('1,234,567')
})
it('should compute trend icon', () => {
const fixture = TestBed.createComponent(MetricCardComponent)
fixture.componentRef.setInput('trend', 'up')
expect(component.trendIcon()).toBe('arrow_upward')
})
})
Tester les patterns resource()
L’API resource() s’intègre avec les utilitaires de test d’Angular. Vous pouvez fournir des loaders mockés, ou utiliser TestBed pour intercepter les appels HTTP comme auparavant.
describe('DashboardComponent', () => {
it('should load dashboard data for selected period', async () => {
const mockService = jasmine.createSpyObj('AnalyticsService', ['loadDashboard'])
mockService.loadDashboard.and.returnValue(Promise.resolve(mockDashboardData))
TestBed.configureTestingModule({
providers: [{ provide: AnalyticsService, useValue: mockService }],
})
const fixture = TestBed.createComponent(DashboardComponent)
fixture.componentRef.setInput('selectedPeriod', 'quarter')
await fixture.whenStable()
expect(mockService.loadDashboard).toHaveBeenCalledWith('quarter')
})
})
Les tests E2E restent inchangés
Les tests Playwright et Cypress qui interagissent avec votre application via le navigateur ne sont pas affectés par la migration vers les signals. Le rendu produit est identique, seul le mécanisme de réactivité interne change. C’est l’un des arguments les plus solides en faveur du maintien d’une suite de tests E2E robuste pendant la migration : elle valide le comportement indépendamment de l’implémentation interne.
Mesurer la migration dans votre application
Il n'existe pas de pourcentage universel crédible pour une migration vers les signals. Mesurez une référence sur des appareils représentatifs, puis rejouez les mêmes parcours après chaque modification. Examinez le travail de détection de changement dans Angular DevTools, le temps du thread principal, le JavaScript transféré, la mémoire pendant les sessions longues et les Core Web Vitals utiles à la route. Ne conservez la modification que si le comportement reste correct et que le compromis mesuré en vaut la peine.
Pièges courants de la migration pour les équipes d’entreprise
Migrer trop de composants à la fois
L’erreur la plus fréquente consiste à tenter de migrer un feature module entier en un seul sprint. Cela crée une surface de régression importante et complique l’isolement des problèmes. Migrez un composant à la fois, validez avec les tests, puis mergez.
Oublier de mettre à jour la syntaxe des templates
Dans les templates, les valeurs des signals doivent être appelées comme des fonctions. Oublier les parenthèses, écrire {{ title }} au lieu de {{ title() }}, renvoie l’objet signal lui-même au lieu de sa valeur. Le compilateur Angular détecte la plupart de ces erreurs, mais les templates construits dynamiquement peuvent lui échapper.
Convertir RxJS à outrance
Tous les observables n’ont pas vocation à devenir des signals. Les flux qui représentent des événements dans le temps, l’orchestration asynchrone complexe et les flux de données temps réel restent mieux modélisés avec RxJS. Convertissez l’état, pas les flux.
Ignorer le piège d’effect()
effect() s’exécute à chaque changement de l’un des signals qu’il lit. Dans les applications d’entreprise comportant de nombreux signals interconnectés, un effect peut se déclencher plus souvent que prévu, provoquant des problèmes de performance ou des boucles infinies. Utilisez computed() pour l’état dérivé. Réservez effect() aux effets de bord qui doivent réellement s’exécuter (logging, analytics, synchronisation avec un système externe), et limitez au strict minimum les dépendances de signals.
Ne pas mettre à jour les wrappers de bibliothèques tierces
Les applications d’entreprise comportent souvent des composants wrapper autour de bibliothèques tierces (graphiques, cartographie, éditeurs de texte enrichi). Ces wrappers ont besoin de stratégies de mise à jour conscientes des signals. La bibliothèque elle-même n’a pas besoin d’utiliser les signals, mais le wrapper doit faire le pont entre la réactivité des signals et l’API impérative de la bibliothèque.
Calendrier de migration pour les équipes d’entreprise
Utilisez la séquence suivante comme exemple de planification, pas comme calendrier promis :
- Premier périmètre : migrez des composants feuilles et des services simples, puis validez avec la suite de tests existante.
- Périmètre suivant : migrez des composants intermédiaires et adoptez les nouvelles primitives uniquement lorsqu'elles simplifient le code.
- Revue de préparation : testez la compatibilité zoneless, le rendu serveur et les wrappers tiers dans un environnement non critique.
- Déploiement : étendez la migration seulement si les tests de régression et les mesures de production confortent la décision.
Il ne s’agit pas d’une réécriture, mais d’une transformation incrémentale qui livre de la valeur à chaque phase. Votre application reste déployable tout au long du processus.
Migrez un composant à la fois
Les signals et la détection de changement zoneless sont stables, mais aucun des deux ne dispense de tests propres à l'application. Avancez par périmètres limités, conservez RxJS lorsque les flux restent l'abstraction la plus claire et laissez les mesures décider si le mode zoneless justifie la migration. Si votre équipe a besoin d'aide pour une migration Angular, parlons-en.
Sources primaires
- Guide Angular sur les signals, consulté le 27 août 2026
- Guide Angular sur le mode zoneless, consulté le 27 août 2026
Vous développez avec Angular ?
Des bibliothèques de composants aux produits complets, nous avons déployé Angular à grande échelle sur des dizaines de projets.