DEV Community

loading...
Cover image for XState

XState

Jean-Marie Romelus
Avide de connaissance !
Updated on ・6 min read

Introduction

Récemment j'ai eu un entretien avec un développeur JavaScript expérimenté, il me parlais des différentes dépendances d'un projet créé avec React.js et il m'a parlé du state machine. Étant curieux j'ai commencé à faire quelques recherches et je suis tombé sur la définition du state machine, mais je ne comprenais pas le rapport avec React, on cherchant bien je suis tombé sur la librairie XState 🔥, après avoir parcouru la documentation et après avoir regardé un exemple de code, j'ai eu l'idée de réalisé un petit projet.

C'est quoi XState ?

XState est une bibliothèque pour créer, interpréter et exécuter des machines d'état et des diagrammes d'état, mais ce n'est pas simplement une librairie de gestion d'état et je compte démystifier cette librairie ! Car c'est un outil très efficace pour gérer des états complexes dans une application React.js ou Vue.js.

Le projet !

Alt Text

Nous réaliserons un système d'authentification avec React, qui
accèdera à une page profil si l'utilisateur rentre le bon mot de passe et la bonne adresse email, ou affichera un message d'erreur si les identifiants sont incorrectes.

Commençons ce projet

Nous allons démarrer un nouveau projet React. Exécutez dans votre Terminal ou CMD :

npx create-react-app tuto-xstate && cd tuto-xstate

Une fois que l'application React est opérationnelle, installez les dépendances suivants:

npm i xstate @xstate/react react-router-dom

Ce sont les seules dépendances que nous aurons besoin.

Le context

Créez un dossier context et à l'intérieur un fichier MachineContext.js en exécutant:

mkdir src/context && touch src/context/MachineContext.js

MachineContext.js

src/context/MachineContext.js

C'est dans ce fichier que nous allons configurer notre Machine.

import React, { createContext } from 'react';
import { Machine, assign } from 'xstate';
import { useMachine } from '@xstate/react';

const authMachine = Machine({
  id: 'signIn',
  initial: 'disconnected',
  context:{
    user: null,
    error: ''
  },
})
  • La propriété id est utilisée pour identifier une machine. Supposons que nous ayons plusieurs machines, nous utiliserons l'ID pour trouver la machine qu'on souhaite.
  • La propriété initial est l'état dans lequel nous voulons que notre machine se trouve.
  • La propriété context nous permet de stocker des données dans la machine, puis le transmettre aux composants qui utilisent la machine.

Maintenant ajoutons la propriété on, c'est elle qui recevra l'événement de l'utilisateur. LOGIN sera le type d'événement à envoyer à la Machine et target déclenche la transition événement. Exemple si vous envoyez LOGIN l'état passera dans authentification started.

const authMachine = Machine({
  id: 'signIn',
  initial: 'disconnected',
  context:{
    user: null,
    error: ''
  },
  on: {
    LOGIN: {
      target: 'authentication.started'
    }
  },
  states: {
    authentication:{
      states:{
        started: {},
        success: {},
        failure: {}
      }
    },
    disconnected: {}
  }
})

Nous allons ajouter la propriété invoke à authentification started, cette propriété attend une promesse pour passer soit dans la transition onDone à condition que la promesse soit résolu ou à onError lorsqu'il y a un problème.

  • Un resolve() passera dans la transition onDone
  • Un reject() passera dans la transition onError
const authMachine = Machine({
on: {
    LOGIN: {
      target: 'authentication.started'
    }
  },
  states: {
    authentication:{
      states:{
        started: {
          invoke: {
            id: 'login',
            src: login,
            onDone: {
              target: 'success',
              actions: assign({ user: (context, event) => event.data })
            },
            onError: {
              target: 'failure',
              actions: assign({ error: (context, event) => event.data.error })
            }
          }
        },
        success: {},
        failure: {}
      }
    },
})

La propriété src nous permet d'utiliser une fonction qui retournera la promesse que l'invoke attend. Dans onDone et onError il y'a la propriété actions avec la fonction assign importé depuis la dépendance xstate, elle permet de mettre à jour le context de la Machine plus précisément user et error dans notre cas.

Créons la fonction login !

La fonction login prend deux paramètres, context et event. Cette fonction peut directement mettre à jour le context de la Machine et l'objet event contient les valeurs transmise par un composant.

const login = (context, event) => new Promise((resolve, reject)=>{
  const { email, password } = event;

  if(email !== 'john.doe@gmail.com' || password !== 'azerty'){
    return reject({ error: 'Le mot de passe ou l\'email est incorrect !' })
  }

  return resolve({ email, password });

});

Maintenant nous allons utiliser createContext() afin de fournir à nos différents composants, l'état actuel de la Machine et la possibilité d'envoyer un événement pour passer dans les différentes transitions et mettre à jour le context de la Machine.

export const MachineContext = createContext();

const MachineContextProvider = ({children}) => {

  const [state, send] = useMachine(authMachine);

  return (
    <MachineContext.Provider value={{state, send}}>
      {children}
    </MachineContext.Provider>
  );

}

export default MachineContextProvider;

Le hook useMachine fonctionne de la même manière que le hook useState de React, il prend en argument notre Machine et retourne l'état actuel et une fonction pour la mettre à jour.

Nos différents composants

Créons ces composants en exécutant depuis votre terminal:

mkdir src/components && touch src/components/{Login,PrivateRoute,Profile}.js

Login.js

src/components/Login.js

Notre composant Login.js doit être en relation avec notre Machine, voila pourquoi nous devons importer MachineContext et consommer son état avec le hook useContext, cela nous permettra d'extraire le state et la fonction send.

import React, { useState, useContext } from 'react';
import { MachineContext } from '../context/MachineContext';
import { Redirect } from 'react-router-dom';

const Login = () => {

    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');

    const { state , send } = useContext(MachineContext);

    const { error } = state.context;

    const handleSubmit = e =>{
        e.preventDefault();

        send('LOGIN', { email, password });        
    };

    return (
        <form className="form" onSubmit={handleSubmit}>  
            <h2>Connexion</h2>
            {state.matches('authentication.failure') && <div style={{color: 'red'}}>{error}</div>}
            <div>
                <input type="text" onChange={e => setEmail(e.target.value)} placeholder="Email"/>
            </div>
            <div>
                <input type="password" onChange={e => setPassword(e.target.value)} placeholder="Mot de passe"/>
            </div>
            <div>
            <button>Se connecter</button>
            {state.matches('authentication.success') && <Redirect to="/profile"/>}
            </div>
        </form>
    )
}

export default Login;

Comme vous pouvez le voir ci-dessus la fonction handleSubmit nous permettra d'envoyer à notre Machine l'événement LOGIN et un objet contenant le mot de passe et l'email. Dans le cas ou l'un des identifiants soit incorrect, la Machine passera dans la transition onError et finira dans la propriété failure de notre Machine donc state.matches('authentication.failure') qui est par défaut une valeur falsy passera à true et error affichera le message d'erreur que nous avons indiqué. Si tout se passe bien onDone sera déclencher et state.matches('authentication.success') passera à true et la redirection vers la page profile se fera avec success.

PrivateRoute.js

src/components/PrivateRoute.js

Nous devons donner un accès privés à la page Profile, accessibles uniquement à un utilisateur authentifié. De ce fait, nous allons créer un composant PrivateRoute. Dans ce composant nous allons importer MachineContext, puis on se servira du useContext pour extraire le state afin d'accéder au context de notre Machine pour s'assurer que l'utilisateur (user) soit bien authentifié, s'il ne l'est pas l'utilisateur sera systématiquement rediriger vers la page de connexion.

import React, { useContext } from 'react';
import { Route, Redirect } from 'react-router-dom';
import { MachineContext } from '../context/MachineProvider';

const PrivateRoute = ({ component: Component, ...rest }) => {

    const { state } = useContext(MachineContext);
    const user = state.context.user;

    return (
        <Route 
        {...rest} 
        render={props =>
        user ? ( 
            <Component {...props}/> 
        ) : (
            <Redirect
                to={{
                    pathname: '/',
                    state: { from: props.location }
                }}
            />
        )}
        />
    );
}

export default PrivateRoute

Profile.js

src/components/Profile.js

Voici le composant de la page profile.

import React, { useContext } from 'react';
import { MachineContext } from '../context/MachineContext';

const Profile = () => {
    const { state } = useContext(MachineContext);
    const user = state.context.user;

    return (
        <div>
            <div>Vous êtes connecté !</div>
            {JSON.stringify(user)}
        </div>
    )
}

export default Profile;

App.js

src/App.js

MachineProvider est le composant qui fournira à tout nos autres composants (Login.js, PrivateRoute.js, Profile.js) la possibilité de consommer l'état de notre Machine.

import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import Profile from './components/Profile';
import Login from './components/Login';
import PrivateRoute from './components/PrivateRoute';
import MachineProvider from './context/MachineProvider';


function App() {
  return (
    <MachineProvider>
      <Router>
        <Switch>
            <Route exact path="/" component={Login}/>
            <PrivateRoute path="/profile" component={Profile}/>
        </Switch>
      </Router>
    </MachineProvider>
  );
}

export default App;

Et voila notre système d'authentification est officiellement opérationnel.

Le repo du projet fini: code source

Quelle est la différence entre Redux et XState ?

Redux n'a pas de méthode intégrée pour gérer les effets secondaires. Il existe de nombreuses options, comme redux-thunk, redux-saga, etc. Alors que XState rend les actions (effets secondaires) déclaratives et explicites, car l'objet State est renvoyé à chaque transition (état actuel + événement).

Mon sentiment

XState est une solution très facile à mettre en place et très intuitif. Cette librairie m'a permit de générer des tests pour chacun de mes états et transitions. J'ai pu obtenir une visualisation claire du chemin qu'un utilisateur emprunte. C'était un vrai plaisir d'écrire cette article, d'ailleurs grâce à cela j'ai pu drastiquement consolidé mes connaissance de cette librairie, qui me sera surement très utile dans un de mes futurs projets. Je vous invite à consulter sa documentation pour une utilisation plus avancée.

Discussion (2)

Collapse
pierreminiggio profile image
Pierre Miniggio

Salut ! Je compte faire un jeu sur un site web en react avec des websocket et une API, et je pensais utiliser une state machine pour la faire (j'en ai pas encore fait, mais j'ai l'impression que ça se prête bien à mon projet vu que c'est un jeu tour par tour)

  • Est-ce que la meilleure approche c'est de créer une seule state machine qui gère toute l'application (formuaire d'inscription, formulaire de connexion, et tous les états du jeu) ?
  • Ou bien est-ce que c'est mieux de faire 1 state machine par élément (inscription, connexion et le jeu) et de faire une 4ème state machine qui gère les changement d'état entre ces 3 fonctionnalités ?
Collapse
romelus profile image
Jean-Marie Romelus Author

Salut,

Désolé pour le temps de réponse.

Je te conseil de créer une machine par fonctionnalité se sera plus simple, car si il y'a une casse tu pourra rapidement trouver d'ou elle vient.

Bon courage 🙂