MAISON CODE .
/ Architecture · XState · React · Testing · Engineering Patterns

Macchine a stati: rendere impossibili gli stati impossibili

Perché i booleani "isLoading" generano bug. Un approfondimento su Finite State Machines (FSM), Statecharts e XState per una logica dell'interfaccia utente a prova di proiettile.

AB
Alex B.
Macchine a stati: rendere impossibili gli stati impossibili

Diamo un’occhiata a un tipico componente scritto da uno sviluppatore junior.

“tsx const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); const [dati, setData] = useState(null);

const fetchData = asincrono () => { setIsLoading(true); prova { const res = attendono api.get(); setData(ris); } cattura (sbaglia) { setIsError(vero); } setIsLoading(falso); // Bug: cosa succede se si verifica un errore? scissione logica. };


**Il bug**: se il blocco `catch` viene eseguito, `isError` diventa vero. Quindi viene eseguito "setIsLoading(false)".
Ma cosa succede se l'utente fa clic su "Riprova"? Utilizzare nuovamente l'avvio del recupero. "isLoading" è vero. Anche `isError` è vero (dall'esecuzione precedente).
L'interfaccia utente mostra contemporaneamente uno spinner E un messaggio di errore.
Questo è uno **Stato impossibile**.

Puoi risolverlo aggiungendo `setIsError(false)` all'inizio.
Quindi aggiungi "isSuccess". Ora hai 3 booleani. $2^3 = 8$ combinazioni possibili. Solo 4 sono validi. Il 50% del tuo spazio statale è costituito da bug.

Noi di **Maison Code Paris** ottimizziamo l'affidabilità. Rifiutiamo la "zuppa booleana". Utilizziamo **Macchine a stati**.


## Perché Maison Code ne parla

In **Maison Code Paris**, agiamo come la coscienza architettonica dei nostri clienti. Spesso ereditiamo stack "moderni" costruiti senza una comprensione fondamentale della scala.

Discutiamo di questo argomento perché rappresenta un punto di svolta critico nella maturità ingegneristica. Implementarlo correttamente differenzia un MVP fragile da una piattaforma resiliente di livello aziendale.


## Perché il codice Maison modella formalmente la logica

Costruiamo dashboard finanziari in cui un "problema tecnico" non è solo fastidioso; è una responsabilità.
Utilizziamo State Machines (XState) per garantire la correttezza:
* **Sicurezza**: gli stati impossibili (Caricamento + Errore) sono matematicamente irrappresentabili.
* **Documentazione**: Il codice *è* il diagramma. Esportiamo diagrammi di stato per mostrare alle parti interessate esattamente come funziona il flusso di pagamento.
* **Testabilità**: generiamo automaticamente test di copertura del percorso al 100% dalla definizione della macchina.
Non speriamo che funzioni; dimostriamo che funziona.

## La Macchina a Stati Finiti (FSM)

Una macchina a stati è un modello di comportamento. È composto da:
1. **Stati**: (ad esempio, `idle`, `loading`, `success`, `failure`).
2. **Eventi**: (ad esempio, `FETCH`, `RETRY`, `CANCEL`).
3. **Transizioni**: (ad esempio, `idle` + `FETCH` -> `loading`).

Fondamentalmente, la macchina può trovarsi solo in **uno stato** alla volta.
Se sei effettivamente nello stato di caricamento e si verifica l'evento FETCH (doppio clic dell'utente), la macchina lo ignora (a meno che tu non lo consenta esplicitamente).
Le condizioni di gara svaniscono.

## XState: La Biblioteca

Usiamo **XState**. È lo standard per gli FSM in JavaScript.
Implementa **Statecharts** (standard W3C SCXML), che consente:
* **Stati gerarchici** (Genitore/Figlio).
* **Stati paralleli** (regioni ortogonali).
* **Stati della storia** (Ricordando dove eri rimasto).

### Implementazione

"dattiloscritto".
importa {creaMachine, assegna} da 'xstate';

const fetchMachine = createMachine({
  id: 'recupera',
  iniziale: 'inattivo',
  contesto: {
    dati: nulli,
    errore: nullo,
  },
  afferma: {
    inattivo: {
      su: { FETCH: 'caricamento' }
    },
    caricamento: {
      // Invoca una promessa (servizio)
      invocare: {
        src: 'fetchData',
        il Fatto: {
          obiettivo: "successo",
          azioni: assegna({ dati: (contesto, evento) => evento.data })
        },
        errore: {
          obiettivo: 'fallimento',
          azioni: assegna({ errore: (contesto, evento) => evento.data })
        }
      }
    },
    successo: {
      // Stato del terminale? O forse consentire l'aggiornamento
      il: { AGGIORNA: 'caricamento in corso' }
    },
    fallimento: {
      il: { RIPROVA: 'caricamento' }
    }
  }
});

Notare la chiarezza. Puoi “RIPROVARE” quando “successo”? No. La transizione non è definita. Puoi eseguire il comando “FETCH” durante il “caricamento”? No. La logica è rigorosa in base alla progettazione.

Guardie e contesto

A volte, le transizioni sono condizionali. “L’utente può passare allo stato pagamento SOLO SE formIsValid è vero.”

“dattiloscritto”. // Guardia il: { SUCCESSIVO: { obiettivo: “pagamento”, cond: (contesto) => contesto.formIsValid } }


Ciò sposta effettivamente la "logica aziendale" fuori dal livello di visualizzazione (componenti di reazione) e nel livello di modello (macchina).
Il componente React diventa stupido. Rende semplicemente lo stato e invia eventi.
`machine.send('AVANTI')`. Non importa *se* sarà il prossimo. La macchina decide.

## Stati paralleli (regioni ortogonali)

Le app reali sono complesse.
Immagina un widget di caricamento.
1. Sta caricando un file (0% -> 100%).
2. L'Utente può Minimizzare/Ingrandire il widget.

Questi sono indipendenti. Puoi "caricare" E "minimizzare".
XState gestisce questa operazione tramite **Stati paralleli**.

"dattiloscritto".
afferma: {
  processo di caricamento: {
    iniziale: 'in sospeso',
    stati: { in sospeso: {}, caricamento in corso: {}, completo: {} }
  },
  interfaccia utente: {
    iniziale: 'espanso',
    stati: { espanso: {}, ridotto a icona: {} }
  }
}

Visualizzatore e Comunicazione

La caratteristica migliore di XState è il Visualizzatore. Puoi copiare e incollare il tuo codice in “stately.ai/viz” e verrà generato un diagramma interattivo. Lo utilizziamo per comunicare con i Product Manager. PM: “L’utente non dovrebbe essere in grado di annullare una volta iniziato il pagamento.” Dev: “Guarda il diagramma. Non c’è alcuna freccia CANCEL dallo stato processing_payment.” Allinea il Modello Mentale con il Modello del Codice.

Test basati su modelli

Poiché l’implementazione attuale è un grafico, possiamo generare test automaticamente. @xstate/test può calcolare il percorso più breve per ogni stato. Genererà un piano di test:

  1. Inizia da “inattivo”.
  2. Spara “FETCH”.
  3. Aspettatevi il “caricamento”.
  4. Risolvi la promessa.
  5. Aspettatevi il “successo”.

Ti garantisce una copertura del 100% dei tuoi flussi logici.

10. Il modello dell’attore (attori XState)

Una singola macchina è fantastica. Ma cosa succede se hai 10 widget di caricamento? Non vuoi un gigantesco uploadMachine. Vuoi generare 10 piccoli uploadActor. La macchina madre (la pagina) comunica con l’attore figlio (il widget) tramite messaggi (send({ type: 'UPLOAD_COMPLETE' })). Questo è il Modello di attore (reso popolare da Erlang/Elixir). Fornisce isolamento. Se un attore si blocca, non si blocca l’intera app. XState rende tutto questo banale usando spawn().

11. Progettisti di stati visivi

Perché scrivere codice? Stately.ai ti consente di trascinare e rilasciare le caselle per progettare la logica. Poiché il codice è il diagramma (isomorfo), il progettista esporta il JSON importato dal codice. Ciò apre le porte alla logica low-code gestita da ingegneri senior. Garantisce che le “Regole aziendali” siano visibili alle parti interessate, non nascoste negli spaghetti useEffect.

13. Stati gerarchici (stati composti)

Un Checkout non è solo un elenco di stati. Ha fasi.

  • Check-out:
    • Spedizione: (indirizzo, metodo)
    • Pagamento: (card_entry, 3ds_verification) Se annulli il Pagamento, torni alla Spedizione? Con gli stati gerarchici, puoi passare a “checkout.shipping.history”. Ciò ci consente di modellare flussi complessi senza “esplosione di stati”. Raggruppiamo gli stati correlati in un nodo genitore. Il genitore gestisce gli eventi globali (ad esempio, le transizioni di LOGOUT a home da qualsiasi sottostato).

14. Test: il modello è il test

Con “@xstate/test”, non scriviamo test E2E manuali per ogni clic sul pulsante. Scriviamo Asserzioni di percorso.

  • Nello stato “spedizione”, afferma “getByText(“Indirizzo di spedizione”)`.
  • Nello stato “pagamento”, affermare “getByText(“Credit Card”)`. La libreria esegue quindi un attraversamento del grafico diretto (percorso più breve) ed esegue Puppeteer/Playwright. Genera automaticamente centinaia di test. Se aggiungi un nuovo stato, i test si aggiornano da soli.

15. Gestire l’esplosione dello stato

La più grande critica agli FSM è l‘“esplosione dello stato”. Se hai 10 booleani, hai €2^{10} = 1024€ stati. Elencarli tutti in una macchina è impossibile. Soluzione: Stati Paralleli (Ortogonalità). Invece di loading_and_modal_open, loading_and_modal_closed, success_and_modal_open… Hai due regioni: data: { caricamento, successo } E ui: { modal_open, modal_closed }. Ciò riduce la complessità da Moltiplicativo (€M * N€) ad Additivo (€M + N€).

16. Verifica formale (sicurezza)

Poiché XState è un grafico matematico, possiamo dimostrare matematicamente le cose. “È possibile raggiungere lo stato di pagamento senza passare per la spedizione?” Possiamo eseguire un algoritmo grafico per verificare se esiste un percorso. In caso affermativo, la compilazione fallisce. Questa è una Verifica formale. Di solito è riservato alla NASA/Avionica, ma XState lo porta nei moduli React. Questo ci dà la certezza che la nostra logica di “gatekeeping” è indistruttibile.

17. Conclusione

Le macchine a stati aggiungono verbosità. La scrittura di una macchina richiede più tempo di “useState”. Ma per i flussi complessi (Checkout, Onboarding, Procedure guidate), il ROI è enorme. Scambia la “Velocità di implementazione” con “Velocità di manutenzione” e “Affidabilità”.

Crediamo che La logica dell’interfaccia utente sia progettazione di algoritmi. Merita un modello formale.


Pagamento difettoso?

Sono presenti condizioni di gara nel flusso di pagamento?

Refactoring su macchine a stati. Assumi i nostri architetti.