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
LOGIN
powoduje przejście do stanuloading
- Stąd akcja
RESOLVE
powoduje przejście do stanusuccess
, z którego akcjąLOGOUT
można przejść z powrotem do stanuunauthorized
- Akcja
REJECT
powoduje przejście do stanufailure
, z którego akcjaRETRY
przeniesie nas do stanuunathorized
ale 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.