Imaginez que vous maintenez une application Angular créée il y a plusieurs années, avant l’arrivée des Signals. Vous avez suivi le parcours de mise à niveau et utilisez maintenant Angular 20 ou 21. Une question se pose naturellement :

Faut-il migrer certaines parties de votre base de code vers les Signals ?

La réponse est : pas nécessairement. Une réécriture complète est rarement pertinente. Mais dans bien des cas, introduire les Signals de manière ciblée peut simplifier votre architecture, réduire le code répétitif et faciliter la compréhension des flux de données.

La vraie question devient alors :

Quelles parties de votre application existante, basée sur RxJS, méritent réellement d’être migrées ?

Dans cette série d’articles, nous allons nous concentrer sur un seul sujet : identifier les zones où le passage aux Signals a le plus d’impact, et voir comment effectuer la migration de façon sûre et progressive.

Aujourd’hui, nous allons nous intéresser à la gestion des inputs et outputs des composants, ainsi qu’à leur état local.

1. Pourquoi commencer par là ?

Si votre application a été créée avant l’introduction des Signals dans Angular, une grande partie de la communication entre vos composants repose encore sur les classiques @Input() / @Output(), parfois associés à ngOnChanges et à des modifications impératives.

Migrer tous les Inputs et Outputs vers les Signals input() et output() à l’aide des schematics officiels d’Angular donne immédiatement à votre base de code une « surface Signals », sans rien réécrire d’autre pour le moment :

  • Ils sont entièrement rétrocompatibles Ils ne changent pas la façon dont votre composant est utilisé depuis l’extérieur.

  • Ils apportent immédiatement la réactivité basée sur les Signals Un @Input() qui nécessitait auparavant ngOnChanges, une conversion manuelle ou une logique de suivi devient un signal réactif que vous pouvez lire directement.

  • Angular fournit des schematics de migration automatiques Vous n’avez pas besoin de modifier des centaines de composants à la main. La CLI s’en charge.

  • Ils ouvrent la voie à l’étape suivante : les valeurs calculées Dès que vos Inputs sont des signals, la logique qui en découle devient bien plus simple à écrire et à comprendre.

2. Remplacer les getters et setters

Dans les anciennes applications Angular, on utilisait souvent le schéma suivant :

  • recevoir un input
  • le transformer dans un setter
  • stocker localement la valeur transformée

Exemple :

private _rawDate!: string;
formattedDate!: string;

@Input() set date(value: string) {
  this._rawDate = value;
  this.formattedDate = this.format(value);
}

private format(date: string): string {
  return new Date(date).toLocaleDateString();
}

Inconvénients fréquents :

  • flux impératif
  • état local mutable
  • la véritable source de vérité est répartie entre _rawDate et formattedDate
  • il est facile de perdre de vue ce qui met à jour quoi

Reprenons maintenant la même fonctionnalité et migrons-la avec l’API des signals :

date = input<string>();

formattedDate = computed(() =>
  new Date(this.date()).toLocaleDateString()
);

Ce que vous y gagnez :

  • aucune logique de setter ni de hook de cycle de vie
  • la valeur transformée est exprimée de façon déclarative
  • l’état reste cohérent
  • le code est bien plus facile à lire, tester et maintenir

3. Remplacer le hook de cycle de vie ngOnChanges

Dans les composants « legacy », il est très courant de réagir à la modification d’un input pour :

  • recalculer une valeur formatée
  • déclencher un effet secondaire (journalisation, appel d’API, analytique, synchronisation d’un service…)
  • faire tout cela à la fois
@Input() date!: string;

formattedDate!: string;

ngOnChanges(changes: SimpleChanges) {
  if (changes.date) {
    // Recalculer la valeur
    this.formattedDate = new Date(this.date).toLocaleDateString();

    // Déclencher un effet secondaire (ici, un simple console.log pour garder l’exemple court et lisible)
    console.log('Date changed:', this.formattedDate);
  }
}

Inconvénients fréquents (nous en avons déjà évoqué certains dans l’exemple précédent) :

  • logique impérative, pilotée par le cycle de vie
  • l’état dérivé (formattedDate) est mutable et peut devenir incohérent
  • les effets secondaires sont dissimulés dans le cycle de vie d’Angular
  • le code est plus difficile à tester, simuler ou réutiliser

Les Signals permettent naturellement de séparer les responsabilités :

  • computed() regroupe toute la logique dérivée et pure
  • effect() réagit aux changements et déclenche les effets secondaires

Réécrivons cette fonctionnalité avec l’API des signals :

date = input<string>();

formattedDate = computed(() =>
  new Date(this.date()).toLocaleDateString()
);

logDateEffect = effect(() => {
  console.log('Date changed:', this.formattedDate());
});

Ce que vous y gagnez :

  • formattedDate est déclaratif et en lecture seule
  • la logique des effets secondaires est explicite et proche de l’état dont elle dépend
  • plus de hooks de cycle de vie ni de SimpleChanges
  • un modèle mental bien plus simple : état => état dérivé => effets

Conclusion

En conclusion, cette voie de migration est facile à suivre et comporte très peu de risques. La mise à jour des Inputs et Outputs est en grande partie prise en charge par les schematics, et elle élimine rapidement de nombreuses anciennes pratiques, comme ngOnChanges ou les getters supplémentaires qui n’ont plus lieu d’être avec les Signals.

Elle apporte aussi des améliorations claires à la détection des changements, ce qui rend votre application plus efficace et plus prévisible. Et surtout, vous pouvez faire tout cela sans toucher à vos services ni à votre logique RxJS. C’est une petite étape progressive qui rend déjà le code plus clair et plus facile à maintenir.

Dans le prochain article, nous verrons comment remplacer le schéma courant de partage d’état dans un service via un BehaviorSubject par des Signals, pour adopter une approche plus simple et réactive.