Articles

Macchine a stati a confronto: XState vs. Robot

Gestire lo stato in React può diventare ingombrante quando la logica dell’applicazione diventa sempre più complessa. Le librerie di terze parti come Redux, Flux e MobX aiutano, ma anche questi strumenti hanno il loro overhead.

Una macchina a stati, chiamata anche macchina a stati finiti o automi a stati finiti, è un modello matematico di calcolo. È una macchina astratta con un numero finito di stati in qualsiasi momento.

In questa guida, esamineremo le somiglianze, le differenze, i pro e i contro di due macchine a stati – XState e Robot – e vedremo come usarle per semplificare la gestione dello stato nelle applicazioni React.

Perché usare una macchina a stati?

Lo stato è una parte importante di molte applicazioni frontend, specialmente in React. Pensate allo stato come una rappresentazione della parte di un’applicazione che cambia.

Considerate un componente che recupera dati da un’API.

const Todo = () => { const = useState(); const handleClick = () => { fetch('https://jsonplaceholder.typicode.com/todos/1') .then(response => response.json()) .then(todo => setData(data.push(todo)) .catch(error => console.error(error) ) } return( <div> <button onClick={handleClick}> Fetch Data </button> {data && data.map(todo => (<p key={todo.id}> {todo.title} <span> {todo.completed} </span></p>) )} </div> ); }

In questo esempio, i dati sono il nostro stato poiché è la parte dell’applicazione che cambia ogni volta che si verifica un evento – in questo caso, il click di un pulsante. Il problema con questa configurazione è che può diventare complicata.

Cosa succede mentre l’utente aspetta che il record venga recuperato o se si verifica un errore durante il recupero? Abbiamo bisogno di aggiungere più stati per gestire questi problemi.

const Todo = () => { const = useState(false); const = useState(); const = useState(false); const handleClick = () => { setLoading(true); fetch('https://jsonplaceholder.typicode.com/todos/1') .then(response => response.json()) .then(todo => { setLoading(false); setData(data.push(todo)); }) .catch(error => { setLoading(false); setIsError(true); }) } return( <div> {loading && <p> Loading Data... </p>} <button onClick={handleClick}> Fetch Data </button> {data && data.map(todo => (<p key={todo.id}> {todo.title} <span> {todo.completed} </span></p>) )} {error && <p> An error occured. Try again.</p>} </div> ); } 

Se la vostra applicazione è complessa, le cose possono rapidamente andare fuori controllo man mano che vengono aggiunte nuove caratteristiche, rendendo il vostro codice difficile da capire, testare e migliorare.

Le macchine a stati affrontano questo problema unico in modo diverso. Con le macchine a stati, si possono definire tutti gli stati in cui la nostra applicazione può trovarsi, le transizioni tra gli stati e gli effetti collaterali che possono verificarsi. Questo vi aiuta ad evitare una situazione in cui l’applicazione si trova in uno stato impossibile.

State Diagram

State Diagram

La nostra applicazione può trovarsi nei seguenti stati:

  1. ready – lo stato iniziale quando l’applicazione si avvia
  2. loading – quando si verifica un evento i.e viene cliccato un pulsante
  3. success – quando il caricamento si risolve
  4. error – quando il caricamento viene rifiutato

L’applicazione passa da uno stato all’altro quando viene attivata un’azione – cioè, quando un utente fa clic su un pulsante. Avete un migliore controllo della vostra applicazione quando potete anticipare tutti i possibili stati in cui può trovarsi.

Cosa fanno XState e Robot?

Secondo la sua documentazione ufficiale, XState è una libreria per creare, interpretare ed eseguire macchine a stati finiti e statecharts, così come gestire invocazioni di queste macchine come attori. È stata creata da David Khourshid per affrontare i problemi di stato nelle interfacce utente.

Robot è una libreria leggera, funzionale e immutabile creata da Mathew Philips per costruire macchine a stato finito. È stata ispirata da XState, Statecharts, e dal linguaggio di programmazione P.

Prequisiti

Per seguire questo tutorial, avrete bisogno di:

  • Conoscenza di JavaScript
  • Conoscenza di React
  • yarn o npm v5.2 o superiore
  • Node versione 10 o superiore

Inizio

Per dimostrare le similitudini e le differenze tra XState e Robot, creeremo un’applicazione che recupera dati da un’API.

Aprire un terminale e inizializzare un’applicazione React.

npx create-react-app state-machine

Questo crea un’applicazione React chiamata State Machine.

In seguito, creare un servizio per recuperare i dati dall’API.

cd src && touch fetchTodo.js

Il comando precedente crea un file chiamato fetchTodo.js nella directory src.

export const fetchTodo = () => { return fetch('https://jsonplaceholder.typicode.com/todos/1') .then((response) => response.json()) .then((todo) => todo);};

Fondamentalmente, ogni volta che la funzione fetchTodo viene chiamata, restituisce i dati recuperati dall’API.

Installazione

XState può essere installato usando npm o yarn o incorporando lo script attraverso un CDN.

Per installare la libreria usando npm, aprire un terminale ed eseguire:

npm install xstate @xstate/react

Questo installa la libreria xstate core e un pacchetto per React chiamato @xstate/react che permette di usare gli hook personalizzati di XState nelle applicazioni React.

Puoi installare Robot usando npm o yarn, ma non un CDN.

Per installare Robot, lancia un terminale ed esegui il seguente comando.

npm install robot3 react-robot

Robot offre anche un pacchetto per utilizzare gli hook personalizzati in React chiamato react-robot

Creazione di una macchina

Prima di poter utilizzare una macchina a stati, devi prima definirla.

Nella directory src, crea un file chiamato xstateMachine.js. Copia il codice qui sotto nel file creato.

import { Machine, assign } from 'xstate';import { fetchTodo } from '../fetchTodo';export const xstateMachine = Machine({ id: 'clickButton', initial: 'ready', context: { todo: null, }, states: { ready: { on: { CLICK: 'loading', }, }, loading: { invoke: { id: 'fetch-todo', src: fetchTodo, onDone: { target: 'success', actions: assign({ todo: (context, event) => event.data, }), }, onError: 'error', }, }, success: { on: { CLICK: 'loading', }, }, error: { on: { CLICK: 'loading', }, }, },});

Le macchine sono definite usando la funzione factory Machine(). La macchina che abbiamo definito nel codice sopra è composta da ID, stati, contesti, azioni e transizioni. Gli ID sono usati per identificare i nodi di stato.

Gli stati nella macchina sono:

  • ready
  • loading
  • success
  • error

Context è uno stato esteso che viene usato per rappresentare dati quantitativi come numeri, stringhe arbitrarie, oggetti, ecc. Lo stato iniziale dell’applicazione è definito come ready. Quando un pulsante viene cliccato, avviene una transizione, spostando lo stato da ready a loading.

Nello stato loading, c’è una proprietà invoke che è responsabile della risoluzione o del rifiuto di una promessa. Ogni volta che la promessa fetchTodo viene risolta, lo stato loading passa allo stato success e l’azione assign aggiorna il contesto con il risultato ottenuto dalla promessa. Se viene rifiutata, passa allo stato error.

Creare una macchina con Robot è simile, anche se con alcune differenze chiave. Una grande differenza è che, poiché Robot è una libreria funzionale, la maggior parte delle azioni sono eseguite usando funzioni, a differenza di XState, che usa oggetti opzione.

Crea un file chiamato robotMachine.js nella tua directory src e incolla quanto segue.

 import { createMachine, invoke, reduce, state, transition } from 'robot3';import { fetchTodo } from '../fetchTodo';const context = () => ({ todo: {},});export const robotMachine = createMachine( { ready: state(transition('CLICK', 'loading')), loading: invoke( fetchTodo, transition( 'done', 'success', reduce((ctx, evt) => ({ ...ctx, todo: evt.data })) ), transition( 'error', 'error', reduce((ctx, ev) => ({ ...ctx, error: ev.error })) ) ), success: state(transition('CLICK', 'loading')), error: state(transition('CLICK', 'loading')), }, context);

In Robot, le macchine sono create usando la funzione createMachine, che accetta un oggetto. Uno stato è definito usando la funzione state e può accettare un transition come parametro.

Il passaggio da uno stato all’altro è fatto con la funzione transition, che accetta l’evento e lo stato successivo come parametri. Opzionalmente, la funzione reduce può essere aggiunta alla transition come terzo parametro. Le funzioni Reduce prendono come parametro una funzione reducer, che è usata per aggiornare il contesto.

Robot ha anche una funzione invoke, simile alla proprietà invoke di XState. Quando l’applicazione è nello stato loading, viene chiamata la funzione invoke. La funzione invoke è un tipo di stato che invoca una promessa e restituisce una funzione o un’altra macchina. Se la funzione invoke risolve la promessa, invia un evento done. Se viene rifiutata, invia un evento error.

Costruire il componente

Ora che la nostra macchina è pronta, il prossimo passo è costruire un componente che utilizzerà la macchina.

Crea un file nella tua directory src per il componente e incolla il seguente.

import React from 'react';import { useMachine } from '@xstate/react';import { xstateMachine } from './stateMachine';function Todo() { const = useMachine(xstateMachine); const { todo } = current.context; return ( <div> <button onClick={() => send('CLICK')}>Fetch Todo XState</button> {current.matches('loading') && <p>loading...</p>} {current.matches('success') && ( <p key={todo.id}> {todo.title} <span> {todo.completed} </span> </p> )} {current.matches('error') && <p>An error occured</p>} </div> );}export default Todo;

Per utilizzare una macchina dobbiamo importare l’hook useMachine dalla @xstate/react library così come la macchina che abbiamo creato in precedenza.

L’hook useMachine è un React Hook che interpreta una macchina. È responsabile dell’avvio di un servizio da eseguire durante il ciclo di vita di un componente.

L’hook useMachine accetta una macchina come parametro e restituisce un array. L’array contiene lo stato current e send, che è una funzione che invia un evento al servizio creato dall’hook useMachine.

Lo stato current è un oggetto che contiene lo stato, il contesto e alcune funzioni di utilità. Per controllare lo stato attuale, usate la proprietà matches, che restituisce un booleano. Quando un utente fa clic sul pulsante, invia un evento al servizio. Quindi controlla lo stato corrente della macchina e rende l’UI appropriata in base allo stato.

L’approccio di Robot alla costruzione dei componenti è simile. Un componente costruito con Robot assomiglierebbe a questo:

import React from 'react';import { useMachine } from 'react-robot';import { robotMachine } from './robotMachine';function Todo() { const = useMachine(robotMachine); const { todo } = current.context; return ( <div> <button onClick={() => send('CLICK')}>Fetch Todo Robot</button> {current.name === 'loading' && <p>loading...</p>} {current.name === 'success' && ( <p key={todo.id}> {todo.title} <span> {todo.completed} </span> </p> )} {current.name === 'error' && <p>An error occured</p>} </div> );}export default RobotTodo;

Robot ha anche un gancio useMachine a cui si può accedere importando il react-robot library. La differenza nell’implementazione è nel modo in cui uno stato viene confrontato. Mentre XState usa la proprietà matches, che è una funzione che accetta la stringa che stiamo cercando di confrontare, Robot usa la proprietà name per controllare lo stato corrente prima del confronto.

Conclusione

Le macchine a stati offrono un modo molto meglio organizzato per gestire lo stato nelle applicazioni React e sono facili da scalare rispetto ad altre alternative. XState e Robot sono due librerie molto popolari che, pur essendo molto simili, affrontano la gestione delle macchine da punti di vista molto diversi.

Il repository di questo tutorial è disponibile su GitHub.

Per maggiori informazioni, controlla le seguenti risorse.

  • Documentazione su Robot
  • Documentazione su XState
  • “UI infinitamente migliori con gli automi finiti” (video)

Visibilità completa sulle applicazioni React in produzione

Il debug delle applicazioni React può essere difficile, specialmente quando gli utenti riscontrano problemi difficili da riprodurre. Se sei interessato a monitorare e tracciare lo stato di Redux, a far emergere automaticamente gli errori JavaScript e a tracciare le richieste di rete lente e il tempo di caricamento dei componenti, prova LogRocket. LogRocket Dashboard Free Trial Banner

LogRocket è come un DVR per le app web, che registra letteralmente tutto ciò che accade sulla vostra app React. Invece di indovinare perché i problemi si verificano, è possibile aggregare e segnalare lo stato della vostra applicazione quando si è verificato un problema. LogRocket monitora anche le prestazioni della tua app, segnalando con metriche come il carico della CPU del client, l’utilizzo della memoria del client, e altro ancora.

Il pacchetto middleware LogRocket Redux aggiunge un ulteriore livello di visibilità nelle tue sessioni utente. LogRocket registra tutte le azioni e lo stato dei vostri negozi Redux.

Modernizza il modo in cui esegui il debug delle tue app React – inizia il monitoraggio gratuitamente.