# Signals, La Réactivité d’Angular Simplifiée

Dans cet article, vous trouverez :

* Une brève histoire de la gestion de *state* dans Angular
    
* Pourquoi Angular adopte les *signals*
    
* Des exemples de gestion de *state* entre trois composants utilisant l'API @Input et @Output
    
* Un exemple montrant comment les *signals* simplifient la gestion de *state*
    
* Une explication des *signals*
    
* Vue d'ensemble de l'API des *signals*
    
* L'avenir d'Angular sans zone et basé sur les *signals*
    

### Brève histoire de la gestion de state dans Angular

L'histoire de la gestion de *state* dans Angular a connu de grands changements et plusieurs évolutions. À ses débuts, avec AngularJS, il n'existait aucun modèle formel de gestion de *state*. C'est l'une des raisons qui ont poussé l'équipe Angular à repenser entièrement le framework et, en 2016, à introduire Angular 2. Angular 2 a marqué un tournant avec un flux de données unidirectionnel, contrairement à AngularJS où la liaison bidirectionnelle était la norme.

Dans AngularJS, les références aux objets enfants étaient maintenues dans le composant parent, ce qui signifiait qu'une mise à jour dans l'enfant affectait automatiquement le parent. Bien que, dans Angular 2, il ait été encore possible d'utiliser la liaison bidirectionnelle grâce à `NgModel()`, cette approche a progressivement été remplacée par l'API `@Input()` et `@Output()`, favorisant une liaison unidirectionnelle. Dans ce modèle, le parent reste informé des changements dans l'enfant, mais si l'enfant doit modifier une valeur stockée dans le parent, il doit envoyer un événement via `@Output()`. Cela impliquait que la gestion explicite des événements était toujours nécessaire.

Le flux unidirectionnel poussait clairement vers une structure hiérarchique, où les composants étaient organisés sous forme d'un arbre. Il encourageait également fortement le schéma consistant à diviser les composants entre "intelligents" et "représentationnels". Cependant, les règles n'étaient pas strictement définies, car Angular permettait encore l'utilisation de services injectés partout dans l'application, souvent utilisés pour partager des données entre plusieurs composants.

### Complexité des applications lourdes

Dans les applications lourdes et complexes axées sur les données, les développeurs utilisaient souvent des bibliothèques de gestion de *state* globales comme NgRx ou NgXS. Bien que cela rendait possible la gestion des *states* complexes, cela ajoutait également de la complexité et beaucoup de code répétitif (boilerplate). En conséquence, le débogage des applications devenait parfois difficile, notamment pour retracer l'origine des modifications d'un *state*.

### Transition vers les "Signals"

Afin de résoudre ces problèmes de longue date liés au modèle original de détection des changements et à la gestion de *state* des applications, l'équipe Angular se dirige de plus en plus vers les *signals*. Les *signals* visent à réduire le besoin d'utiliser des bibliothèques de gestion de *states* lourdes et complexes.

### Exemple pratique

Prenons l'exemple de deux composants différents partageant un *state* commun. Auparavant, il était très courant d'utiliser des services combinés avec RxJS pour gérer cet *state*. Dans une application Angular typique, il était nécessaire de s'abonner explicitement aux modifications des valeurs. Voici un aperçu de ce fonctionnement traditionnel avant l'arrivée des "*signals*".

```typescript
// service

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class RacletteService {
  private slicesSubject = new BehaviorSubject<number>(0);
  slices$ = this.slicesSubject.asObservable();

  addSlice() {
    this.slicesSubject.next(this.slicesSubject.value + 1);
  }

  removeSlice() {
    this.slicesSubject.next(0, this.slicesSubject.value - 1);
  }

  resetSlices() {
    this.slicesSubject.next(0);
  }
}
```

* `slicesSubject` conserve la valeur actuelle et émet cette valeur à tous les abonnés.
    
* `slices$` est un observable auquel les composants peuvent s'abonner.
    
* Le service, utilisé comme un "mini-store," contient des méthodes pour mettre à jour le *state*.
    

Le parent doit écouter les événements et mettre à jour le *state*. Le service doit mettre à jour la valeur, et tous les composants utilisant cette valeur doivent s'abonner aux changements.

Ainsi, nous aurions typiquement un composant qui met à jour le *state* :

```typescript
import { Component } from '@angular/core';
import { RacletteService } from './raclette.service';

@Component({
  selector: 'raclette-controls',
  template: `
    <button (click)="raclette.addSlice()">Add Slice 🧀</button>
    <button (click)="raclette.removeSlice()">Remove Slice 🧀</button>
    <button (click)="raclette.resetSlices()">Reset 🧀</button>
  `
})
export class RacletteControlsComponent {
  constructor(public raclette: RacletteService) {}
}
```

et d'autres composants qui liraient cette *state* :

```typescript
import { Component, OnInit } from '@angular/core';
import { RacletteService } from './raclette.service';

@Component({
  selector: 'raclette-display',
  template: `<p>Slices on the plate: {{ slices }}</p>`
})
export class RacletteDisplayComponent implements OnInit {
  slices = 0;

  constructor(private raclette: RacletteService) {}

  ngOnInit() {
    this.raclette.slices$.subscribe(value => {
      this.slices = value;
    });
  }
}
```

Tous les composants utilisant ce *state* devraient être abonnés à ses changements et les écouter afin de mettre à jour la vue en conséquence.  
On peut constater que, si notre composant lit de nombreux *states* différents, il pourrait rapidement être surchargé par un grand nombre d'abonnements lors de son initialisation. Si les manipulations des *states* ne sont pas simples, nous nous retrouverions souvent à devoir imbriquer nos observables ou les consommer simultanément en utilisant `.switchMap`, `.mergeMap`, `combineLatest`, etc.

Imaginez que nous souhaitons ajouter des pommes de terre dans notre `RacletteService` et lire leur *state* en parallèle avec les *states* des tranches de fromage pour calculer un *state* dérivé :

```typescript
// service

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class RacletteService {
  private potatoesSubject = new BehaviorSubject<number>(0);
  potatoes$ = this.potatoesSubject.asObservable();

  addPotato() {
    this.potatoesSubject.next(this.potatoesSubject.value + 1);
  }

  removePotato() {
    this.potatoesSubject.next(this.potatoesSubject.value - 1);
  }
}
```

Ensuite, dans notre composant d'affichage, nous ferions très souvent quelque chose comme ceci :

```typescript
import { Component, OnInit } from '@angular/core';
import { RacletteService } from './raclette.service';
import { combineLatest } from 'rxjs';

@Component({
  selector: 'raclette-display',
  template: `
    <p>Raclette slices: {{ slices }}</p>
    <p>Potatoes: {{ potatoes }}</p>
    <p>Total items: {{ totalItems }}</p>
  `
})
export class RacletteDisplayComponent implements OnInit {
  slices = 0;
  potatoes = 0;
  totalItems = 0;

  constructor(private raclette: RacletteService) {}

  ngOnInit() {
    combineLatest([this.raclette.slices$, this.raclette.potatoes$])
      .subscribe(([slicesValue, potatoesValue]) => {
        this.slices = slicesValue;
        this.potatoes = potatoesValue;
        this.totalItems = slicesValue + potatoesValue;
      });
  }  
}
```

Ou une version moins propre que l'on voit souvent dans les applications Angular :

```typescript
ngOnInit() {
  this.raclette.slices$.subscribe(slicesValue => {
    this.slices = slicesValue;
    this.raclette.potatoes$.subscribe(potatoesValue => {
      this.potatoes = potatoesValue;
      this.totalItems = slicesValue + potatoesValue;
    });
  });
}
```

Examinons maintenant comment les *signals* changent le gestion de *state* d'Angular :

Avant les *signals*, notre *state* était partagé. Avec les *signals*, tout *state* peut être rendu réactif par défaut, et le signal dans le template réagit automatiquement à ses changements. Plus besoin d'abonnements, ni d'émettre des événements pour indiquer à Angular qu'une valeur est susceptible de changer.

Nous ne dépendons plus de RxJS et Zone.js pour suivre le *state* de notre application. Les *signals* nous offrent une réactivité fine. Zone.js effectuait un monkey-patching sur toutes les tâches asynchrones, ce qui entraînait le déclenchement d'une détection de changement complète à travers tout l'arbre des composants. En revanche, les *signals* sont des primitives réactives (reactive primitives) qui suivent explicitement les dépendances. Lorsqu'un signal est mis à jour, Angular sait quels composants consomment ce signal et ne met à jour que ceux-ci, pas tout l'arbre.

Avec une réactivité fine, les *signals* apportent également des moyens plus simples de gérer le *state* et moins de code. Prenons le même code écrit avec les *signals* :

```typescript
// raclette.service.ts
import { Injectable, signal, computed } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class RacletteService {
  slices = signal(0);
  potatoes = signal(0);
  totalItems = computed(() => this.slices() + this.potatoes());

  addSlice() {
    this.slices.update(n => n + 1);
  }

  removeSlice() {
    this.slices.update(n => 0, n - 1);
  }
  addPotato() {
    this.potatoes.update(n => n + 1);
  }

  removePotato() {
    this.potatoes.update(n => 0, n - 1);
  }
}
```

Composant 1 :

```typescript
import { Component } from '@angular/core';
import { RacletteService } from './raclette.service';

@Component({
  selector: 'raclette-controls',
  template: `
    <button (click)="raclette.addSlice()">Add Slice 🧀</button>
    <button (click)="raclette.removeSlice()">Remove Slice 🧀</button>
    <button (click)="raclette.resetSlices()">Reset 🧀</button>
    <button (click)="raclette.addPotato()">Add Potato 🥔</button>
    <button (click)="raclette.removePotato()">Remove Potato 🥔</button>
  `
})
export class RacletteControlsComponent {
  constructor(public raclette: RacletteService) {}
}
```

Composant 2 :

```typescript
import { Component } from '@angular/core';
import { RacletteService } from './raclette.service';

@Component({
  selector: 'raclette-display',
  template: `
    <p>Raclette slices: {{ raclette.slices() }}</p>
    <p>Potatoes: {{ raclette.potatoes() }}</p>
    <p>Total items: {{ raclette.totalItems() }}</p>
  `
})
export class RacletteDisplayComponent {
  constructor(public raclette: RacletteService) {}
}
```

Nous pouvons immédiatement comprendre pourquoi, avec les *signals*, les abonnements deviennent souvent inutiles. Les *signals* d'Angular "enveloppent" une valeur et notifient tous les consommateurs lorsque cette valeur est modifiée. De plus, nous bénéficions d'une gestion et d'un accès plus simples aux valeurs calculées, sans besoin d'utiliser `combineLatest()`. Le code devient ainsi plus clair et plus lisible.

Pour parvenir à cela, l'équipe Angular s'est tournée vers certaines fonctionnalités avancées de TypeScript.  
Examinons ce qui se cache vraiment derrière la "magie" des *signals* d'Angular.

L'API ressemble à ceci :

```typescript
type Signal<T> = (() => T) & {
  [SIGNAL]: unknown;
}
```

Expliquons cela :

`(() => T)` — est une fonction type, ce qui signifie que notre signal est appelable comme n'importe quelle autre fonction. Lorsqu'on l'appelle, il retourne une valeur de type `T`. Cela nous permet d'écrire du code comme ceci :

```typescript
const slices: Signal<number> = () => 3;

slices(); // 3
```

Mais en même temps, notre signal est une propriété marquée (branded property) :

```typescript
{
  [SIGNAL]: unknown;
}
```

Cette propriété SIGNAL n'est pas destinée à être utilisée et existe uniquement pour marquer (ou plus précisément pour identifier) une valeur comme une valeur signal. Elle retourne `unknown` car nous ne nous soucions pas de la valeur qu'elle renvoie.

Le typage du signal comme valeur d'intersection (`&`) indique une valeur qui est à la fois une fonction (`(() => T)`) et possède une propriété SIGNAL spéciale (`{ SIGNAL: unknown; }`). Cette propriété SIGNAL ajoutée empêche notre compilateur de traiter toutes les fonctions comme des *signals*, car seules les valeurs créées en tant que *signals* peuvent correspondre à ce type :

```typescript
type Signal<T> = () => T;
const fn = () => 123;
const s: Signal<number> = fn; // notre signal peut etre une fonction
```

Mais le code suivant générera une erreur :

```typescript
const fn = () => 123; 
const s: Signal<number> = fn; // notre fonction ne peut pas être un signal car    //manque la propriété [SIGNAL].
```

Ainsi, un signal Angular est une valeur qui :

1. peut être appelée comme une fonction retournant `T`, **et**
    
2. possède une propriété dont la clé est exactement `SIGNAL`.
    

De plus, l'équipe Angular nous offre une option qui, si nous marquons notre signal comme modifiable (writable), nous donne accès à une API permettant de gérer son *state*, comme `set()` pour définir le *state* du signal ou `update()` pour calculer une nouvelle valeur à partir de l'ancienne. Nous avons également la possibilité de retourner la valeur du signal comme étant en read-only.

```typescript
interface WritableSignal<T> extends Signal<T> {
  set(value: T): void;
  update(updateFn: (value: T) => T): void;
  asReadonly(): Signal<T>;
  override [SIGNAL]: unknown;
}
```

### Signals : Vers un Angular plus Moderne

Un autre grand avantage des *signals* est la prévention des fuites mémoire. Il n'est plus nécessaire de se désabonner comme c'était le cas avec les Observables.  
Avec les *signals*, la "subscription" est automatique dans le template. Les fuites mémoire sont plus difficiles à produire, car Angular détruira les références des signals lorsque l'utilisateur quittera la page, c'est-à-dire lorsque le composant sera détruit.

En introduisant les *signals*, Angular évolue vers des frameworks modernes, plus réactifs et flexibles. Nous pouvons observer cette idée se développer avec Angular 21 (en développement, prévu en novembre 2025), qui sera désormais zoneless par défaut.  
ZoneJS utilisait les événements DOM et les tâches asynchrones pour indiquer quand le *state* "pourrait" changer. Cela provoquait de nombreux problèmes de performance. En le supprimant, on garantit que la détection des changements ne se déclenche plus sur toutes les opérations asynchrones.

Angular 21 exploite également les *signals* pour gérer le *state* des formulaires en introduisant (encore en version préliminaire) des formulaires alimentés par les *signals*. Plus d'informations à ce sujet dans le prochain article.

#### Ressources supplémentaires :

**Documentation officielle Angular** :

* Sur les "*Signals*" : [https://angular.dev/guide/signals](https://angular.dev/guide/signals)
    
* Sur le "Zoneless" : [https://angular.dev/guide/zoneless](https://angular.dev/guide/zoneless)
    

L’IMAGE: [https://unsplash.com/fr/photos/homme-tenant-de-la-poudre-rose-xW4bS\_Rj5Po?utm\_source=unsplash&utm\_medium=referral&utm\_content=creditShareLink](https://unsplash.com/fr/photos/homme-tenant-de-la-poudre-rose-xW4bS_Rj5Po?utm_source=unsplash&utm_medium=referral&utm_content=creditShareLink)
