Utiliser des states machines avec React et XState

Jerome Boileux
Yousign Engineering & Product
5 min readJan 8, 2024
Photo by Paul Zoetemeijer on Unsplash

Nous allons voir dans cet article comment, en pratique, utiliser les librairies xstate & xstate/react afin de simplifier l’orchestration des états et de nos composants React.

Note: Nous utiliserons dans les exemples la version 4 de XState, une version 5 est désormais disponible et l’api peut changer, mais le concept reste le même.

🔗 https://xstate.js.org/docs

Actuellement, monter en compétence sur l’utilisation des machines à états dans React peut sembler intimidant au début. Cependant, une fois quelques concepts débloqués, vous découvrirez que l’utilisation de XState peut grandement simplifier la gestion des états dans vos composants React

Ce n’est pourtant pas aussi complexe qu’il n’y parait ; et après quelques déclics assez satisfaisants à débloquer, on se surprend à trouver de plus en plus de cas d’utilisation aux states machines.

Il faut dire que les problématiques adressées peuvent, à priori, être résolues d’autres manières et souvent avec l’attirail classique (des states, des context, voire des reducers). Peut être que les ressources manquent un peu dès qu’il s’agit de passer de la théorie à la pratique. Mais les règles métier ne restant pas toujours basiques, recourir à l’utilisation de states machines peut rapidement s’avérer pertinent dans ses composants.

XState peut être utilisé avec React pour :

  • Orchestrer l’état local dans un composant
  • Partager et gérer un état plus global entre plusieurs composants de manière performante
  • Consommer les données d’autres hooks

Gérer les états d’un composant

Pour le premier point, c’est très simple à implémenter, illustrons le cas d’un bouton toggle avec 2 états inactive / active.

En utilisant createMachine de XState et le hook useMachine depuis xstate/react on pourrait écrire le composant de la façon suivante :

La librairie permet de générer automatiquement le diagramme d’état correspondant, ce qui est très utile pour partager le fonctionnel.

simple toggle flow

Comparons cela à une implémentation utilisant simplement le hook useState.

Maintenant supposons qu’il soit possible de reset automatiquement l’état “actif” depuis un autre bouton, avec un compteur par exemple.

Aussi nous pourrions donner la possibilité à l’utilisateur d’activer / désactiver entièrement la fonctionnalité depuis un bouton switch.

Il faudra alors, dans l’exemple ci dessus, implémenter la logique du compteur, puis gérer de multiples nouveaux états sur ce composant, notamment des états qui ne doivent pas se produire fonctionnellement : quid de l’action du compteur alors que le bouton est inactif.

Et ainsi de suite, l’ajout de fonctionnalités va progressivement introduire des états censés être non atteignables mais qu’il faudra pourtant gérer dans le code et empêcher.

C’est là qu’utiliser une state machine prend tout son sens.

XState est une bibliothèque permettant de créer, interpréter et exécuter des machines à états finis

un peu de théorie 👉 la state machine est un modèle mathématique de calcul qui décrit le comportement d’un système qui est dans un seul état à la fois.

Implémentons par exemple ces nouvelles fonctionnalités dans le premier exemple.

Nous allons ajouter un context dans la machine, puis utiliser le concept de guard (condition sur les events), ainsi qu’une action automatique avec after.

complex toggle flow

On voit aisément comment XState va rendre nos composants et le métier plus simples à lire, plus simples à maintenir, plus simples à faire évoluer.

💡 Faisons l’essai d’implémenter ces fonctionnalités supplémentaires dans l’exemple sans XState.

Cela donnerait quelque chose comme ceci

Maintenant supposons que les specifications fonctionnelles changent, il faut désormais inverser les boutons et proposer que le bouton principal propose d’auto reset lorsque la feature est active, tout en intercalant une dialog d’avertissement, puis que le secondaire force la désactivation.

Reprenons l’exemple ci dessus sans xstate.

On remarque qu’on a largement dû retravailler le composant, inverser des conditions, ajouter des méthodes… on note aussi que les booléens et les conditions s’accumulent et rendent le composant plus complexe. Il faudrait commencer à songer à l’utilisation d’un reducer pour éviter les doubles states et conditions.

Maintenant implémentons ces changements dans l’exemple avec XState.

Commençons par la machine

Ici, les modifications sont essentiellement dans la machine (hormis l’ajout d’un composant pour la dialog) et assez simple à faire. Le composant reste clair car découplé du “métier”.

Comment orchestrer plusieurs composants depuis une machine ?

Mais souvent dans les applications “réelles”, l’architecture est plus complexe et on trouve un arbre de composants qui doivent partager un state plus global et de la donnée.

Ici encore, XState peut nous aider à énormément simplifier l’implémentation de problématiques complexes.

💡 Faites l’essai de ce découpage dans l’exemple sans XState.

Reprenons notre exemple en découpant notre webapp en plusieurs composants :

  • ToggleButton le bouton principal
  • ForceButton le bouton secondaire pour mettre forcer la désactivation
  • SwitchButton le switch global sur la fonctionnalité
  • WarningDialog la dialog d’avertissement avant l’auto reset

Ces composants devront chacun consommer la state machine et lui envoyer des évènements.

L’approche va être d’utiliser React Context pour fournir le “service” aux composants grace aux différents hooks proposés par la librairie xstate/react.

Pour cela nous allons utiliser le hook useInterpret qui fournit une référence statique de la machine et le renseigner dans notre Provider, de cette façon nous garantissons que l’arbre des composants ne va pas se re-rendre à chaque changement dans les états.

const toggleService = useInterpret(toggleMachine)

Les composants vont pouvoir s’abonner au service grace au hook useActor de la façon suivante.

const { toggleStyervice } = useToggleContext();
const [state, send] = useActor(toggleService);

Voici le résultat

use xstate with actor components

Si l’on rajoute à cela la possibilité d’avoir un context plus complexe dans la machine, de pouvoir appeler des services externes comme des mutations, on obtient un outil élégant et très puissant pour :

  • implémenter simplement une UI complexe,
  • découpler l’aspect fonctionnel (la machine représente le métier) de l’implémentation technique (les composants interprètent le métier) ,
  • garantir la lisibilité, la solidité, et la scalabilité des composants.

Conclusion

Chez Yousign, où la simplicité de l’UI cache souvent une complexité technique, nous avons adopté les states machines depuis plusieurs mois.

Bien que la maîtrise des subtilités ait demandé du temps, les exemples d’utilisations sont désormais nombreux et les refactos fleurissent pour simplifier nos composants et consolider notre webapp de signature électronique.

--

--