Les Signals, Explications & Comment Les Implémenter

Le State Management est un paradigme de programmation réactive bien connu des développeurs Web. Il existe de multiples implémentations en fonction de la librairie et / ou du framework frontend choisi. MobXOvermindRefluxZustand, etc… Les plus connus à ce jour sont Redux pour React, mais également Vuex pour Vue ou encore NgRx pour Angular.

Le principe est le suivant, le State Management repose sur un flux de données unidirectionnel pour mettre à jour les données d’une application Web. Ainsi, plus de confusion possible les données sont disponibles à un seul et unique endroit (le store) et ne peuvent être mis à jour que dans un cycle spécifique (on va parler d’immutabilité). Il n’y a alors qu’une source de vérité !

Schéma du flux de données unidirectionnel

L’internaute va déclencher une action depuis l’interface (l’action peut également s’effectuer de manière automatique), puis cette action va être « dispatché » au store pour modifier l’état global de l’application, afin que les composants ayant souscrit / abonné à ce flux de données réagissent au changement et impacter la modification dans leur rendu ; l’internaute déclenche une autre action, etc…

Bien que le pattern ait fait ses preuves depuis de nombreuses années, son implémentation reste néanmoins une charge à appréhender. Pour ce qui est de React, depuis la version 16.8, la simplification de l’API « Context » fait qu’il est possible de créer un State Management « maison » grâce au pattern « reducer » sans pour autant injecter toute la librairie Redux. La mise à l’échelle de la programmation réactive par l’usage d’un store a permis également de voir apparaitre des librairies agnostiques, telle que NanoStores (cf. Astro).

Mais en 2023, est-il toujours aussi performant ? Quid du concept de Signals qui (re)vient sur le devant de la scène, avec notamment Solid ou même Angular (tout récemment) ? Intéressons nous a cela de plus près…

Signals, C’est Quoi ?

Le concept de Signals apparaît dans les années 2010 avec notamment la librairie Knockout qui poussent la réactivité à un niveau inédit (à l’époque) avec des patterns de développement solide tel que les Observables, les effets de bord (side effect), ou encore les états dérivés (pureComputed). Bien qu’innovant, l’objectivité n’est pas au rendez-vous, et les batailles choisies sont ceux d’AngularJS avec notamment la propagation du pattern MVVM (Model-View / View-Model) rendant obsolète jQuery, plutôt que la performance… À cette même époque, on parle de vues et de controllers et surtout de biding bidirectionnel de données d’après le principe suivant : « le modèle met à jour la vue. La vue met à jour le modèle. Mais le modèle est l’unique source de vérité. » En opposition donc au principe de store et de State Management que nous connaissons aujourd’hui…

Pourtant depuis quelques années, les problèmes de performance et de mise à jour du DOM surgissent davantage, pourquoi ? Et bien parce qu’on en demande toujours plus ! Et toujours plus rapidement !!! Si bien que l’équipe en charge du projet Solid remet au gout du jour certaines notions oubliées : les Signals, la réactivité fine pour un framework orienté composants !

Les Signals, sont l’aboutissement des travaux de Knockout, et permettent (au travers d’un système de référence) de mettre la valeur d’une variable sans pour autant (re)rendre le / les composant(s) impacté(s). Ceci est une révolution quand on sait combien coute un rendu de composant pour les frameworks disposant d’un VirtualDOM (React, Vue, Preact…).

Schéma de mise à jour d'un composant sans Signal
Schéma de mise à jour d'un composant avec Signal

Cas d’Usage : Solid

Les Signals faisant parties intégrantes d’un composant Solid, on peut simplement considérer qu’il s’agit de l’état du composant. Voici un cas pratique avec l’exemple d’un compteur classique :

				
					import { createSignal } from 'solid-js';

export default function Counter() {
  const [count, setCount] = createSignal(0);
  const isEven = () => count() % 2 === 0; // Computed

  const increment = () => setCount(count() + 1);
  const decrement = () => setCount(count() - 1);

  return (
    <div class="flex flex-col">
      <span>
        {isEven() ? "Even" : "Odd"} Value: {count()}
      </span>

      <div class="flex">
        <button onClick={decrement}>-1</button>
        <button onClick={increment}>+1</button>
      </div>
    </div>
  );
}
				
			

L’accès à la valeur (.value) s’effectue en appelant le Signal (ici count()) ainsi lorsque setCount() est invoqué permettant de changer l’état immuable du Signal. La valeur de ce dernier est mis à jour mais ne (re)rend pas le composant, puisque sa référence ne change pas !

NB : Il est aussi possible de créer des effets de bord, avec des Signals dérivés, notamment isEven. Dans ce cas précis, l’état dérivé est aussi un Signal dépendant du premier. Il faut donc également appeler le Signal pour accéder à sa valeur.

Solid se démarque comme étant une librairie de composant Web à la réactivité « fine » lui permettant ainsi d’être plus performant que ses concurrents, notamment 2x plus rapide que React !

Proof Of Concept : React

En partant du principe qu’il s’agit d’un changement de valeur et non pas de référence, on peut supposer qu’il est possible de reproduire le même fonctionnement avec le framework React. Imaginons donc l’algorithme suivant :

				
					import { createSignal } from 'solid-js';

export default function Counter() {
  const [count, setCount] = createSignal(0);
  const isEven = () => count() % 2 === 0; // Computed

  const increment = () => setCount(count() + 1);
  const decrement = () => setCount(count() - 1);

  return (
    <div class="flex flex-col">
      <span>
        {isEven() ? "Even" : "Odd"} Value: {count()}
      </span>

      <div class="flex">
        <button onClick={decrement}>-1</button>
        <button onClick={increment}>+1</button>
      </div>
    </div>
  );
}
				
			

L’accès à la valeur (.value) s’effectue en appelant le Signal (ici count()) ainsi lorsque setCount() est invoqué permettant de changer l’état immuable du Signal. La valeur de ce dernier est mis à jour mais ne (re)rend pas le composant, puisque sa référence ne change pas !

NB : Il est aussi possible de créer des effets de bord, avec des Signals dérivés, notamment isEven. Dans ce cas précis, l’état dérivé est aussi un Signal dépendant du premier. Il faut donc également appeler le Signal pour accéder à sa valeur.

Solid se démarque comme étant une librairie de composant Web à la réactivité « fine » lui permettant ainsi d’être plus performant que ses concurrents, notamment 2x plus rapide que React !

Proof Of Work : Vue

Quid du framework Vue ? Sur le même principe que React reproduisons une mécanique de référence et de valeur en créant une fonction personnalisée pour l’API composition :

				
					<template>
  <div class="flex flex-col">
    <span>Value: {{ count() }}</span>

    <div class="flex">
      <button @click="decrement">-1</button>
      <button @click="increment">+1</button>
    </div>
  </div>
</template>

<script setup>
import { shallowRef } from 'vue';

function createSignal(initialValue) {
  const ref = shallowRef(initialValue);
  const get = () => ref.value;
  const set = (val) => {
    ref.value = typeof val === 'function' ? val(ref.value) : val;
  };

  return [get, set];
}

const [count, setCount] = createSignal(0);

const increment = () => setCount(count() + 1);
const decrement = () => setCount(count() - 1);
</script>
				
			

À l’instar de React, ceci fonctionne-t-il (ou pas) ? Bien que Vue s’appuie sur un VirtualDOM pour opérer ses modifications, il supporte étonnamment bien ce mode de fonctionnement (contrairement à son homologue), et provoque ainsi moins de (re)rendu de ses composants.

Mais alors, comment s’y prendre pour faire fonctionner la notion de Signals depuis la librairie React, et ainsi augmenter à la fois les performances ? Preact a la réponse !

Cas d’usage : React

Depuis fin d’année 2022, Preact (librairie alternative à React, disposant d’une API similaire mais d’un VirtualDOM moins gourmand) propose une implémentation de Signals « agnostique » ! À l’image de NanoStores (solution de State Management proposé par Astro), Preact Signals porte le concept de Signals à l’ensemble des frameworks orientés composants ! Avec React il suffit d’une requête à NPM pour disposer de la réactivité « fine » : npm install @preact/signals-react

				
					import { useSignal, useComputed } from '@preact/signals-react';

export default function Counter() {
  const count = useSignal(0);
  const isEven = useComputed(() => count.value % 2 === 0);

  const increment = () => (count.value += 1);
  const decrement = () => (count.value -= 1);

  return (
    <div className="flex flex-col">
      <span>
        {isEven.value ? 'Even' : 'Odd'} Value: {count.value}
      </span>

      <div className="flex">
        <button onClick={decrement}>-1</button>
        <button onClick={increment}>+1</button>
      </div>
    </div>
  );
}
				
			

Et voilà comment bénéficier des Signals depuis React. Alors, certes il ne s’agit pas de l’implémentation « officiel » mais c’est un bon début pour voir arriver une RFC (Request For Comments) pour la / les prochaine(s) version de la librairie.

Quid du State Management dans tout cela ? Figurez-vous qu’il est possible de pousser la notion de Signals un peu plus loin, les utilisant en dehors du composant ! Ajoutez tout cela dans un fichier store.js, exportez vos variables (vos Signals) et votre application disposera d’une unique source de vérité pour des données globales et partagées… Simple !

				
					import { signal, computed } from '@preact/signals-react';

// TODO: Make Store
export const count = signal(0);
export const isEven = computed(() => count.value % 2 === 0);

export default function Counter() {
  const increment = () => (count.value += 1);
  const decrement = () => (count.value -= 1);

  return (
    <div className="flex flex-col">
      <span>
        {isEven.value ? 'Even' : 'Odd'} Value: {count.value}
      </span>

      <div className="flex">
        <button onClick={decrement}>-1</button>
        <button onClick={increment}>+1</button>
      </div>
    </div>
  );
}
				
			

Cas d’Usage : Angular

Depuis mai 2023 (donc plutôt récemment) Angular a été mis à jour (en version 16) pour bénéficier de la fonctionnalité de Signals « out-of-the-box« . Alors, comment ça marche ?

				
					import { Component, signal, computed } from '@angular/core';

@Component({
  selector: 'app-counter',
  template: `
    <div class="flex flex-col">
      <span>{{ isEven() ? 'Even' : 'Odd' }} Value: {{ count() }}</span>

      <div class="flex">
        <button (click)="decrement()">-1</button>
        <button (click)="increment()">+1</button>
      </div>
    </div>
  `,
})
export class CounterComponent {
  count = signal<number[]>(0);
  isEven = computed<boolean>(() => this.count() % 2 === 0);

  constructor() {}

  increment() {
    this.count().update((value) => value + 1);
  }

  decrement() {
    this.count().update((value) => value - 1);
  }
}
				
			

Plutôt cool, hein ! 😉 Angular utilise actuellement ZoneJS pour parcours son arbre de composants et opérer le / les changement(s), mais avec l’usage des Signals au coeur du framework, ZoneJS est dorénavant moins sollicité pour (re)rendre les composants, puisque (encore une fois) seule la valeur des Signals change et non la référence ! À titre personnel, je pense que l’ajout de cette fonctionnalité au framework de Google va re-populariser ce dernier 👍

Il est également possible de déléguer tout ou partie des Signals depuis un Service, notamment si l’on souhaite bénéficier de données partagées :

				
					import { Injectable, signal, computed } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class CounterService {
  count = signal<number[]>(0);
  isEven = computed<boolean>(() => this.count() % 2 === 0);

  increment() {
    this.count().update(value => value += 1);
  }

  decrement() {
    this.count().update(value => value -= 1);
  }
}
				
			

counter.service.ts

				
					import { Component, signal, computed } from '@angular/core';
import { CounterService } from './counter.service';

@Component({
  selector: 'app-counter',
  // template: ...
})
export class CounterComponent {
  count!: WritableSignals<number>;
  isEven!: WritableSignals<boolean>;

  constructor(private counterService: CounterService) {
    this.count = counterService.count;
    this.isEven = counterService.isEven;
  }

  increment() {
    this.counterService.increment();
  }

  decrement() {
    this.counterService.decrement();
  }
}
				
			

counter.component.ts

Avec l’ajout des Signals dans Angular, on peut dorénavant se passer d’un service, pour déclarer un état global à notre application :

				
					import { signal, computed } from '@angular/core';

export const count = signal<number>(0);
export const isEven = computed<boolean>(() => count() % 2 === 0);

export const increment = () => {
  count.update(value => value += 1);
};

export const decrement = () => {
  count.update(value => value -= 1);
};
				
			

store.ts

				
					import { Component, signal, computed } from '@angular/core';
import { count, isEven, increment, decrement } from './store';

@Component({
  selector: 'app-counter',
  // template: ...
})
export class CounterComponent {
  count: WritableSignals<number> = count;
  isEven: WritableSignals<boolean> = isEven;

  increment() {
    increment();
  }

  decrement() {
    decrement();
  }
}
				
			

counter.component.ts

Okay, c’est très verbeux… Mais cela fonctionne ! Et c’est tellement simple 😮 La prochaine bataille à mener concerne l’adoption des nouveaux patterns de développement liés à l’usage des Signals, et ceux, quel que soit le framework utilisé ! Stay tuned…

Damien CHAZOULE

Développeur FullStack passionné d’informatique depuis plus d’une décennie…

J’ai commencé à développer des applications mobiles en Java, puis en Kotlin pour Android. Lorsque j’ai découvert NodeJS et le framework AngularJS (repose en paix), je me suis découvert une véritable passion pour le développement Web, mais surtout pour le langage JavaScript.

React, VueJS, TypeScript, MongoDB, GraphQL, SCSS, etc. sont les technologies avec lesquelles je travaille au quotidien. Aujourd’hui, je conçois et maintiens des applications/sites de A à Z (des API au rendu visuel). De plus, j’accorde une attention particulière à l’UI/UX, que je considère comme la pierre angulaire d’un projet réussi !

L’art du développement et ma curiosité pour les nouveautés me poussent à partager mes connaissances avec mes pairs, à travers des articles, des conférences et des formations.

Conscient du caractère chronophage du code, j’aspire à gérer des projets informatiques d’envergure dans le futur. En attendant, j’avance dans ma vie la tête pleine de nouvelles idées, et les yeux tournés vers le Web.

This website stores cookies on your computer. Cookie Policy