Machines à états : rendre les états impossibles impossibles
Pourquoi les booléens « isLoading » engendrent des bugs. Une plongée approfondie dans les machines à états finis (FSM), les Statecharts et XState pour une logique d'interface utilisateur à toute épreuve.
Regardons un composant typique écrit par un développeur junior.
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [données, setData] = useState(null);
const fetchData = async() => {
setIsLoading(true);
essayez {
const res = attendre api.get();
setData(res);
} attraper (erreur) {
setIsError(vrai);
}
setIsLoading(faux); // Bug : Et si une erreur se produisait ? division logique.
} ;
Le bug : si le bloc catch s’exécute, isError devient vrai. Ensuite, setIsLoading(false) s’exécute.
Mais que se passe-t-il si l’utilisateur clique sur « Réessayer » ? Utilisez recommencer la récupération. isLoading est vrai. isError est également vrai (de l’exécution précédente).
L’interface utilisateur affiche simultanément un Spinner ET un message d’erreur.
Il s’agit d’un état impossible.
Vous le corrigez en ajoutant setIsError(false) au début.
Ensuite, vous ajoutez « isSuccess ». Vous avez maintenant 3 booléens. 2€^3 = 8€ de combinaisons possibles. Seulement 4 sont valables. 50 % de votre espace d’état est constitué de bogues.
Chez Maison Code Paris, nous optimisons pour la fiabilité. Nous rejetons la « soupe booléenne ». Nous utilisons des machines à états.
Pourquoi Maison Code en parle
Chez Maison Code Paris, nous agissons comme la conscience architecturale de nos clients. Nous héritons souvent de stacks “modernes” construites sans compréhension fondamentale de l’échelle.
Nous abordons ce sujet car il représente un point de pivot critique dans la maturité de l’ingénierie. Une mise en œuvre correcte différencie un MVP fragile d’une plateforme résiliente de niveau entreprise.
Pourquoi Maison Code modélise formellement la logique
Nous construisons des tableaux de bord financiers où un « problème » n’est pas seulement ennuyeux ; c’est un handicap. Nous utilisons des machines à états (XState) pour garantir l’exactitude :
- Sécurité : Les états impossibles (Chargement + Erreur) sont mathématiquement non représentables.
- Documentation : Le code est le diagramme. Nous exportons des diagrammes d’états-transitions pour montrer aux parties prenantes exactement comment fonctionne le flux de paiement.
- Testabilité : nous générons automatiquement des tests de couverture de chemin à 100 % à partir de la définition de la machine. Nous n’espérons pas que cela fonctionnera ; nous prouvons que cela fonctionne.
La machine à états finis (FSM)
Une machine à états est un modèle de comportement. Il se compose de :
- États : (par exemple, « inactif », « chargement », « succès », « échec »).
- Événements : (par exemple,
FETCH,RETRY,CANCEL). - Transitions : (par exemple,
idle+FETCH->loading).
Surtout, la machine ne peut être que dans un état à la fois. Si vous êtes effectivement dans l’état de « chargement » et que l’événement « FETCH » se produit (l’utilisateur double-clique), la machine l’ignore (sauf si vous l’autorisez explicitement). Les conditions de course disparaissent.
XState : La bibliothèque
Nous utilisons XState. C’est la norme pour les FSM en JavaScript. Il implémente Statecharts (norme W3C SCXML), qui permet :
- États hiérarchiques (Parent/Enfant).
- États parallèles (régions orthogonales).
- États historiques (se souvenir de l’endroit où vous vous êtes arrêté).
Implémentation
importer { createMachine, assign } depuis 'xstate' ;
const fetchMachine = créerMachine({
identifiant : 'récupérer',
initiale : 'inactif',
contexte : {
données : nulles,
erreur : nulle,
},
déclare : {
inactif : {
sur : { FETCH : 'chargement' }
},
chargement : {
// Invoquer une promesse (service)
invoquer : {
src : 'fetchData',
onFait : {
cible : « succès »,
actions : attribuer ({ data : (contexte, événement) => event.data })
},
surErreur : {
cible : « échec »,
actions : attribuer ({ erreur : (contexte, événement) => event.data })
}
}
},
succès : {
// Etat terminal ? Ou peut-être autoriser l'actualisation
sur : { ACTUALISATION : 'chargement' }
},
échec : {
sur : { RÉESSAYER : 'chargement' }
}
}
});
Remarquez la clarté. Pouvez-vous « RÉESSAYER » en cas de « succès » ? Non. La transition n’est pas définie. Pouvez-vous « FETCH » lors du « chargement » ? Non. La logique est stricte par conception.
Gardes et contexte
Parfois, les transitions sont conditionnelles.
“L’utilisateur peut passer à l’état paiement UNIQUEMENT SI formIsValid est vrai.”
// Garde
le : {
SUIVANT : {
cible : 'paiement',
cond : (contexte) => contexte.formIsValid
}
}
Cela déplace efficacement la « logique métier » hors de la couche de vue (composants React) et dans la couche de modèle (machine).
Le composant React devient stupide. Il rend simplement l’état et envoie des événements.
machine.send('NEXT'). Cela s’en fiche si ça continue. La machine décide.
États parallèles (régions orthogonales)
Les vraies applications sont complexes. Imaginez un widget de téléchargement.
- Il télécharge un fichier (0% -> 100%).
- L’utilisateur peut réduire/agrandir le widget.
Ceux-ci sont indépendants. Vous pouvez « télécharger » ET « réduire ». XState gère cela via Parallel States.
déclare : {
Processus de téléchargement : {
initiale : « en attente »,
indique : { en attente : {}, téléchargement : {}, terminé : {} }
},
interface utilisateur : {
initiale : « élargi »,
états : { développé : {}, réduit : {} }
}
}
Visualiseur et communication
La meilleure fonctionnalité de XState est le Visualiseur.
Vous pouvez copier-coller votre code dans « stately.ai/viz » et cela génère un diagramme interactif.
Nous l’utilisons pour communiquer avec les chefs de produit.
PM : “L’utilisateur ne devrait pas pouvoir annuler une fois le paiement commencé.”
Dev : “Regardez le diagramme. Il n’y a pas de flèche CANCEL depuis l’état processing_payment.”
Il aligne le modèle mental avec le modèle de code.
Tests basés sur des modèles
L’implémentation actuelle étant un graphique, nous pouvons générer des tests automatiquement.
@xstate/test peut calculer le Chemin le plus court vers chaque état.
Il générera un plan de test :
- Commencez au « inactif ».
- Lancez « FETCH ».
- Attendez-vous à un « chargement ».
- Résoudre la promesse.
- Attendez-vous à un « succès ».
Il vous garantit une couverture à 100 % de vos flux logiques.
10. Le modèle d’acteur (XState Actors)
Une seule machine, c’est génial.
Mais que se passe-t-il si vous disposez de 10 widgets de téléchargement ?
Vous ne voulez pas d’une « uploadMachine » géante.
Vous voulez générer 10 petits « uploadActor ».
La Machine Parent (La Page) communique avec l’Acteur Enfant (Le Widget) via des messages (send({ type: 'UPLOAD_COMPLETE' })).
Il s’agit du Actor Model (popularisé par Erlang/Elixir).
Cela procure un isolement. Si un acteur plante, cela ne fait pas planter toute l’application.
XState rend cela trivial en utilisant spawn().
11. Concepteurs d’états visuels
Pourquoi écrire du code ? Stately.ai vous permet de glisser-déposer des boîtes pour concevoir la logique. Parce que le code est le diagramme (isomorphe), le concepteur exporte le JSON que votre code importe. Cela ouvre la porte à la Low-Code Logic gérée par des ingénieurs seniors. Il garantit que les « règles métier » sont visibles pour les parties prenantes, et non cachées dans les spaghettis « useEffect ».
13. États hiérarchiques (États composés)
Une caisse n’est pas seulement une liste d’états. Il y a des phases.
- Commandement :
- Expédition : (
adresse,méthode) - Paiement : (
card_entry,3ds_verification) Si vous annulez le « Paiement », revenez-vous à « Expédition » ? Avec les états hiérarchiques, vous pouvez passer à « checkout.shipping.history ». Cela nous permet de modéliser des flux complexes sans « State Explosion ». Nous regroupons les états liés dans un nœud parent. Le parent gère les événements globaux (par exemple, les transitionsLOGOUTvershomedepuis n’importe quel sous-état).
- Expédition : (
14. Tests : le modèle est le test
Avec @xstate/test, nous n’écrivons pas de tests E2E manuels pour chaque clic de bouton.
Nous écrivons des Assertions de chemin.
- Dans l’état « expédition », affirmez « getByText(“Shipping Address”)`.
- Dans l’état
paiement, affirmezgetByText("Credit Card"). La bibliothèque exécute ensuite un parcours de graphe dirigé (chemin le plus court) et exécute Puppeteer/Playwright. Il génère automatiquement des centaines de tests. Si vous ajoutez un nouvel état, les tests se mettent à jour eux-mêmes.
15. Gestion des explosions d’état
La plus grande critique adressée aux FSM est « l’explosion d’État ».
Si vous avez 10 booléens, vous avez €2^{10} = 1024€ états.
Les lister tous dans une machine est impossible.
Solution : États parallèles (orthogonalité).
Au lieu de loading_and_modal_open, loading_and_modal_closed, success_and_modal_open…
Vous avez deux régions : data : {loading, success } ET ui : { modal_open, modal_closed }.
Cela réduit la complexité du multiplicatif (€M * N€) à l’additif (€M + N€).
16. Vérification formelle (sécurité)
Parce que XState est un graphique mathématique, nous pouvons prouver mathématiquement des choses. « Est-il possible d’atteindre l’état « paiement » sans passer par « expédition » ? Nous pouvons exécuter un algorithme graphique pour vérifier si un chemin existe. Si c’est le cas, la construction échoue. Il s’agit d’une vérification formelle. Il est généralement réservé à la NASA/Avionics, mais XState l’apporte aux formulaires React. Cela nous donne l’assurance que notre logique de « Gatekeeping » est incassable.
17. Conclusion
Les machines à états ajoutent de la verbosité. L’écriture d’une machine prend plus de temps que « useState ». Mais pour les flux complexes (Checkout, Onboarding, Wizards), le ROI est énorme. Vous échangez la « vitesse de mise en œuvre » contre la « vitesse de maintenance » et la « fiabilité ».
Nous pensons que UI Logic est une conception d’algorithme. Cela mérite une modélisation formelle.
Caisse de buggy ?
Avez-vous des conditions de concurrence dans votre flux de paiement ?