W poprzednich postach opisywałem sposoby na zarządzanie stanem w React -Redux, Context API, MobX czy MobX State Tree – pewne problemy jednak pozostają nierozwiązane.
Powyższe rozwiązania sprawdzą się jako magazyn na dane – nie powiedzą nam jednak nic o systemie. Nie określą w jednoznaczny sposób możliwych stanów jakie wystąpią podczas działania aplikacji, nie walidują poprawności przejścia pomiędzy jednym stanem a drugim – musi o to zadbać ręcznie programista.
Często też wiedza o tych stanach ginie w trakcie rozwoju projektu – w miarę jak przybywa kodu i funkcjonalności, gdy odchodzą programiści znający system i przychodzą nowi, którzy muszą się go uczyć.
W rozwiązaniu tego problemu sprawdzają się skończone maszyny stanowe – definiują skończoną ilość dostępnych stanów i przejścia z jednego stanu w drugi.
Spis treści
- Czym jest skończona maszyna stanowa
- Biblioteka XState
- Integracja biblioteki XState z Reactem
- Podsumowanie
Czym jest skończona maszyna stanowa
W uproszczeniu o skończonej maszynie stanowej myśleć możesz jako o zestawie wszystkich stanów w jakiej znajduje się model wraz ze zdarzeniami, doprowadzającymi do przejścia z jednego stanu w drugi. Bardzo istotne jest to, że przejścia pomiędzy stanami są jasno określone – ze stanu X można przejść do Y tylko, jeżeli maszyna na to pozwala.
Czemu, jako frontendowiec, w ogóle miałbyś się interesować maszynami stanowymi? Ponieważ uczynią Twoją pracę znacznie prostszą – UI jako maszyna stanowa jest prostszy w zrozumieniu, łatwiej przypilnować przejść z jednego stanu w drugi. Prościej wejść w projekt, jeżeli możesz wejść w jeden plik który opisuje zachowanie danej części systemu zamiast szukać tych informacji po omacku w wielu plikach.
Ponadto, dzięki temu że maszyny stanowe można opisywać diagramami komunikacja z biznesem czy grafikiem za pomocą diagramów będzie dużo bardziej produktywna i zmniejszy się ilość niedomówień.
Biblioteka XState
Jedną z implementacji maszyny stanowej jaką możesz użyć w projekcie jest biblioteka XState.
XState visualizer
Zaletą maszyny stanowej XState (i maszyn stanowych w ogólności) jest to, że możesz ją wizualizować. Biblioteka XState dostarcza narzędzia Visualizer. Możesz nawet na bieżąco podglądać, jak wygląda diagram Twojej maszyny podczas kodowania jej w edytorze na stronie.
Przykładowo maszyna:
const checked = 'checked';
const unchecked = 'unchecked';
const TOGGLE = 'TOGGLE'
const checkbboxMachine = Machine({
id: 'checkbox',
initial: unchecked,
states: {
[unchecked]: {
on: {
[TOGGLE]: checked,
},
},
[checked]: {
on: {
[TOGGLE]: unchecked,
},
},
},
});
Wygląda następująco:

W kodzie maszyny celowo użyłem tzw dynamic property key – jeżeli np zdecydowalibyśmy się na zmianę standardów, np nazwa stanu pisana wielkimi literami a nazwa zdarzenia małymi, zmienić trzeba będzie tylko w jednym miejscu. Gorąco zachęcam Cię do takiego podejścia.
Maszyna stanowa
Diagram bardziej skomplikowanej maszyny stanowej mógłby się przedstawiać następująco:

Logika wygląda tak:
- Na początku maszyna znajduje się w stanie
unathorized - Akcja
LOGINpowoduje przejście do stanuloading - Stąd akcja
RESOLVEpowoduje przejście do stanusuccess, z którego akcjąLOGOUTmożna przejść z powrotem do stanuunauthorized - Akcja
REJECTpowoduje przejście do stanufailure, z którego akcjaRETRYprzeniesie nas do stanuunathorizedale tylko wtedy, jeżeli zostały jakieś próby logowania.
const unauthorized = 'unauthorized';
const LOGIN = 'LOGIN';
const loading = 'loading';
const RESOLVE = 'RESOLVE';
const REJECT = 'REJECT';
const success = 'success';
const failure = 'failure';
const LOGOUT = 'LOGOUT';
const RETRY = 'RETRY';
const authMachine = Machine({
id: 'auth',
initial: unauthorized,
context: {
retries: 5,
},
states: {
[unauthorized]: {
on: {
[LOGIN]: {
target: loading,
cond: 'hasRetries',
},
},
},
[loading]: {
on: {
[RESOLVE]: success,
[REJECT]: failure,
}
},
[success]: {
on: {
[LOGOUT]: unauthorized,
},
},
failure: {
on: {
[RETRY]: {
target: unauthorized,
actions: assign({
retries: (context, event) => context.retries - 1
}),
},
},
},
},
},
{
guards: {
hasRetries: context => context.retries > 0,
},
},
);
Integracja biblioteki XState z Reactem
Stwórz proszę nową aplikację CRA. Po zainstalowaniu biblioteki głównej oraz jej paczki integracji z Reactem:
npm i xstate @xstate/react
Masz do dyspozycji hook zwracający stan oraz funkcję, która umożliwia wysyłanie zdarzeń do maszyny:
const [state, send] = useMachine(authMachine);
state udostępnia funkcję matches do sprawdzania, czy maszyna znajduje się w określonym stanie:
const isLoading = state.matches(loading);
Stwórz UI umożliwiające wysyłanie zdarzeń do maszyny stanowej:
import React from 'react';
import {useMachine} from "@xstate/react";
import {authMachine, failure, loading, LOGIN, LOGOUT, REJECT, RESOLVE, RETRY, success} from "./authMachine";
function App() {
const [state, send] = useMachine(authMachine)
return (
<div className="App">
<div>
Current state: {state.value}
</div>
<button onClick={() => send(LOGIN)}>Login</button>
{state.matches(loading) && (
<p>
<button onClick={() => send(RESOLVE)}>
Success
</button>
<button onClick={() => send(REJECT)}>
Failure
</button>
</p>
)}
{state.matches(success) && (
<p>
<button onClick={() => send(LOGOUT)}>Logout</button>
</p>
)}
{state.matches(failure) && (
<p>
<button onClick={() => send(RETRY)}>Retry</button>
</p>
)}
<p>
<button onClick={() => send(RETRY)}>
Always visible retry
</button>
</p>
</div>
);
}
export default App;
Kod zakłada, że w pliku authMachine znajduje się zdefiniowana wyżej maszyna stanowa – z tą różnicą, że wszystkie stałe są wyeksportowane, by móc użyć ich w pliku głównym.
Jeżeli już przekopiowałeś, uruchomiłeś i przeklikałeś aplikację zauważysz, że klikając przycisk Always visible retry stan zmienia się tylko wtedy, jeżeli maszyna jest w stanie failure. Dzieje się tak, ponieważ przejścia między stanami muszą być zdefiniowane jawnie – upraszcza to konstrukcję UI.
Podsumowanie
Maszyny stanowe pozwalają grupować w jednym miejscu wiedzę o tym, jak działa system. Ich użycie zwiększa przewidywalność zachowań systemu, ułatwiają też komunikację z osobami nietechnicznymi dzięki temu, że można opisać je jako diagram – szczególnie, że biblioteka XState dostarcza dedykowane wizualizacji narzędzie.
Warto rozważyć ich zastosowanie w projektach frontendowych.

