Maszyna stanów w aplikacji webowej - kiedy warto ją stosować?

Diagram przepływu spotkań: stan maszynowy od "Drafted" do "Published", "Suspended", "Closed" lub "Canceled".

Autor

Alex Jabłoński

Opublikowano

22 paź 2026

Spis treści

Interfejs, który raz pokazuje spinner, raz komunikat błędu, a innym razem dane, szybko zamienia się w plątaninę warunków. Model state machine porządkuje takie zachowanie, ponieważ jasno opisuje dostępne stany, zdarzenia oraz przejścia między nimi. Pokażę, jak wykorzystać maszyny stanów w architekturze aplikacji webowych, kiedy zastępują serię instrukcji if, a kiedy lepiej sięgnąć po prostsze rozwiązanie.

Maszyna stanów zamienia niejasną logikę w przewidywalny przepływ

  • Stan opisuje aktualny tryb działania komponentu lub procesu.
  • Zdarzenie wywołuje próbę przejścia do kolejnego stanu.
  • Przejście określa, co wolno zrobić z danego stanu i przy konkretnym zdarzeniu.
  • Warunki ochronne blokują przejście, gdy dane lub uprawnienia są niepoprawne.
  • Największą korzyścią jest przewidywalność, testowalność i łatwiejsze debugowanie.

Czym jest maszyna stanów i po co stosować ją w aplikacji

Maszyna stanów to model obliczeniowy, który w danym momencie znajduje się w jednym z określonego, skończonego zbioru stanów. Reaguje na zdarzenia, a następnie zgodnie z ustalonymi regułami pozostaje w bieżącym stanie albo przechodzi do innego.

Dobrym przykładem jest formularz logowania. Nie wystarczy informacja, że formularz jest „aktywny”. W praktyce może być w stanie idle, submitting, success albo error. Każdy z nich ma inne dozwolone działania. Podczas wysyłania nie chcę uruchamiać kolejnego żądania, a po sukcesie nie powinien pojawić się przycisk ponownej wysyłki.

Właśnie tutaj zwykłe zmienne typu isLoading, hasError i isLoggedIn zaczynają się ze sobą mieszać. Po kilku zmianach łatwo uzyskać niemożliwą kombinację, na przykład jednocześnie aktywny spinner i komunikat sukcesu. Jawnie opisany model ogranicza takie przypadki, bo definiuje dozwolone konfiguracje, zamiast pozwalać aplikacji tworzyć je przypadkiem.

W swojej pracy traktuję maszynę stanów przede wszystkim jako narzędzie architektoniczne, a nie bibliotekę do importowania. Najpierw rozpisuję zachowanie na kartce lub w diagramie, dopiero później decyduję, czy wystarczy kilka funkcji, czy potrzebny będzie rozbudowany silnik.

Z jakich elementów składa się ten model

Najprostsza maszyna ma cztery składniki. Są nimi stany, zdarzenia, przejścia i stan początkowy. W większych rozwiązaniach dochodzą jeszcze akcje, warunki ochronne oraz dane kontekstowe.

Element Znaczenie Przykład w aplikacji
Stan Aktualny tryb działania systemu loading
Zdarzenie Sygnał, który może zmienić stan SUBMIT, RETRY
Przejście Reguła wyboru kolejnego stanu loading + SUCCESS = success
Guard Warunek dopuszczający przejście poprawny token sesji
Akcja Efekt wykonywany przy zmianie lub wejściu do stanu zapisanie danych użytkownika
Kontekst Dane pomocnicze niezastępujące stanu liczba prób, komunikat błędu

Trzeba rozdzielić stan od danych kontekstowych. Informacja error może być stanem, natomiast treść komunikatu, kod odpowiedzi HTTP czy liczba prób należą do kontekstu. Taki podział poprawia czytelność, bo nie tworzę osobnego stanu dla każdego możliwego tekstu błędu.

Przejście nie powinno być rozumiane jako dowolna zmiana wartości. To jawna reguła. Jeżeli aplikacja jest w stanie success, zdarzenie SUBMIT może nie robić nic albo prowadzić do loading, ale taka decyzja musi wynikać z modelu, a nie z przypadkowego efektu ubocznego komponentu.

Przykład minimalnej implementacji w JavaScript

const transitions = {
  idle: {
    SUBMIT: "loading"
  },
  loading: {
    SUCCESS: "success",
    FAILURE: "error"
  },
  error: {
    RETRY: "loading"
  },
  success: {}
};

function transition(currentState, event) {
  return transitions[currentState]?.[event] ?? currentState;
}

let state = "idle";
state = transition(state, "SUBMIT");  // loading
state = transition(state, "SUCCESS"); // success

Ten przykład jest celowo prosty. Funkcja transition nie wykonuje żądań HTTP ani nie zmienia interfejsu. Wyłącznie wylicza kolejny stan, dzięki czemu można ją testować jako czystą funkcję. Efekty uboczne, takie jak pobieranie danych, powinny znajdować się obok modelu, a nie ukrywać w każdej możliwej gałęzi warunku.

Diagram przedstawia state machine z stanami LIST, ADD, EDIT. Strzałki pokazują przejścia między stanami, np.

Jak zaprojektować maszynę stanów dla aplikacji webowej

Projektowanie zaczynam od zdarzeń, które rzeczywiście mogą wystąpić, a nie od nazw komponentów. W formularzu płatności będą to na przykład SUBMIT, PAYMENT_SUCCESS, PAYMENT_FAILED i CANCEL. Dopiero potem ustalam, w jakich stanach te zdarzenia są obsługiwane.

  1. Opisz proces od początku do końca. Zapisz, co dzieje się przed działaniem użytkownika, w trakcie operacji i po jej zakończeniu.
  2. Wypisz skończony zestaw stanów. Każdy stan powinien mieć konkretne znaczenie i wpływać na zachowanie aplikacji.
  3. Zdefiniuj zdarzenia. Oddziel kliknięcia, odpowiedzi API, upływ czasu i błędy sieciowe.
  4. Rozrysuj dozwolone przejścia. Sprawdź, czy każde przejście ma sens i czy nie brakuje ścieżki awaryjnej.
  5. Dodaj warunki oraz efekty. Guard powinien sprawdzać regułę, a akcja wykonywać konkretną operację.
  6. Przetestuj nie tylko sukces. Uwzględnij anulowanie, ponowienie, timeout, odświeżenie strony i wielokrotne kliknięcie.

Przy aplikacjach webowych szczególnie dobrze działa rozdzielenie trzech warstw. Pierwsza opisuje model przejść, druga obsługuje efekty zewnętrzne, a trzecia renderuje interfejs. Dzięki temu komponent Reacta, Vue czy zwykły moduł DOM nie musi samodzielnie odtwarzać reguł biznesowych.

Przykładowy przepływ zamówienia może wyglądać tak: cart przechodzi do pending po potwierdzeniu zakupu, a potem do paid lub payment_failed. Nie pozwalam przejść z cart bezpośrednio do paid, ponieważ płatność musi mieć potwierdzenie z systemu zewnętrznego. Taka reguła jest prostsza do zauważenia na diagramie niż w rozproszonym kodzie.

W większych przepływach przydają się stany hierarchiczne. Stan editing może mieć podstany draft, validating i saving. To ogranicza liczbę połączeń na diagramie, ale nie powinno służyć do ukrywania zbyt skomplikowanej logiki. Jeżeli diagram staje się nieczytelny, dzielę model na mniejsze maszyny komunikujące się zdarzeniami.

Kiedy użyć maszyny stanów zamiast if/else lub reduktora

Nie każdy komponent potrzebuje formalnej maszyny. Dla prostego przełącznika światła dwie wartości i jedna funkcja mogą być wystarczające. Rozwiązanie zaczyna zyskiwać przewagę wtedy, gdy pojawiają się co najmniej trzy lub cztery znaczące stany, asynchroniczne operacje oraz wiele możliwych ścieżek błędu.

Podejście Kiedy pasuje Ograniczenie
if/else Krótka, lokalna logika Szybko tworzy trudną do kontroli sieć warunków
Booleany i zmienne Jedna niezależna informacja Mogą tworzyć sprzeczne kombinacje
Reducer Przewidywalne aktualizowanie stanu aplikacji Nie zawsze jasno pokazuje dozwolone przejścia
Maszyna stanów Procesy, formularze, sesje, płatności, wieloetapowe UI Wymaga zaprojektowania modelu i może być przesadą przy prostych komponentach
Statechart Duże modele z hierarchią, równoległością i historią Większy narzut poznawczy oraz narzędziowy

Reducer odpowiada głównie na pytanie, jak wyliczyć nowy stan po zdarzeniu. Maszyna stanów idzie krok dalej i opisuje, które przejścia są legalne. Oba podejścia można łączyć, ale nie warto budować rozbudowanego modelu tylko dlatego, że dana biblioteka jest popularna.

W ekosystemie JavaScript często spotyka się XState, który pozwala deklarować stany, zdarzenia, guardy, akcje i usługi asynchroniczne. Narzędzie jest sensowne przy złożonych przepływach, zwłaszcza gdy zespół chce wizualizować model i śledzić zdarzenia. Dla jednego przycisku z trzema stanami prosty obiekt przejść będzie zwykle tańszy w utrzymaniu.

Praktyczne zastosowania w projektach webowych

Logowanie i sesja użytkownika

Sesję można opisać stanami logged_out, checking, logged_in i session_expired. Dzięki temu aplikacja nie wyświetli panelu użytkownika przed zakończeniem sprawdzania tokenu, a wygaśnięcie sesji będzie osobnym zdarzeniem, a nie przypadkową zmianą kilku flag.

Formularze i walidacja

Formularz rejestracji często ma stany editing, validating, submitting, success i error. Oddzielenie walidacji lokalnej od wysyłania danych pozwala uniknąć sytuacji, w której przycisk pokazuje „wysyłanie”, mimo że formularz nie przeszedł podstawowych kontroli.

Ładowanie danych z API

Najbardziej typowy przepływ to idle, loading, loaded i failed. Dla bardziej wymagających ekranów dodaję jeszcze refreshing, aby odświeżanie nie zasłaniało użytkownikowi wcześniej pobranych danych. To drobna różnica, ale mocno poprawia odbiór interfejsu.

Przeczytaj również: Wzorce projektowe PHP - czy na pewno ich potrzebujesz?

Proces zamówienia

W sklepie internetowym osobne stany mogą reprezentować cart, checkout, payment_pending, paid, cancelled i refunded. Model pomaga pilnować, aby zwrot był dostępny dopiero po opłaceniu zamówienia, a anulowanie nie uruchamiało się po jego wysłaniu do realizacji.

Te przykłady łączy jedna zasada: stan opisuje tryb działania procesu, a nie wygląd pojedynczego elementu. Jeżeli kilka komponentów musi znać tę samą regułę, maszyna powinna znajdować się bliżej domeny aplikacji niż konkretnego widoku.

Najczęstsze błędy i granice tego podejścia

Pierwszy błąd to tworzenie stanów na podstawie każdej zmiennej. hasModal, isLoading i hasError nie zawsze powinny być trzema niezależnymi stanami. Najpierw pytam, czy opisują różne tryby procesu, czy tylko dane, które można przechowywać w kontekście.

Drugi problem pojawia się wtedy, gdy przejścia wykonują zbyt wiele pracy. Funkcja odpowiedzialna za zmianę stanu nie powinna jednocześnie wysyłać kilku żądań, modyfikować DOM-u i zapisywać danych w magazynie. Lepiej oddzielić decyzję o kolejnym stanie od efektu, który ta decyzja uruchamia.

Trzeci błąd to brak obsługi zdarzeń niemożliwych lub spóźnionych. Odpowiedź API może przyjść po anulowaniu formularza, a użytkownik może kliknąć przycisk dwa razy w ciągu kilkudziesięciu milisekund. Model powinien określać, czy takie zdarzenie jest ignorowane, prowadzi do błędu, czy pozostaje bezpieczne.

Maszyna stanów nie rozwiązuje też automatycznie problemów z trwałością danych, współbieżnością ani komunikacją między usługami. Jeżeli stan ma przetrwać odświeżenie, trzeba go zapisać i poprawnie odtworzyć. Jeżeli kilka procesów zmienia ten sam rekord, potrzebne będą dodatkowe mechanizmy, na przykład blokady, wersjonowanie lub idempotencja.

Uważam też, że diagram nie jest dowodem poprawności. Może wyglądać bardzo profesjonalnie, a mimo to pomijać timeout, brak sieci albo ponowienie operacji. Największą wartość daje dopiero połączenie modelu z testami przejść, w których sprawdzam każdy ważny stan i każde zdarzenie.

Jak zacząć bez komplikowania projektu

Weź jeden konkretny proces, najlepiej taki, który już ma kilka flag i trudnych do wyjaśnienia warunków. Wypisz jego stany na kartce, zaznacz zdarzenia oraz usuń przejścia, które nie mają uzasadnienia biznesowego. Dopiero wtedy przenieś model do kodu.

Na początek wystarczy zwykła mapa przejść i funkcja wyliczająca następny stan. Bibliotekę warto dodać dopiero wtedy, gdy potrzebujesz hierarchii, równoległych regionów, wizualizacji, historii albo współdzielenia modelu przez większy zespół.

Dobrze zaprojektowana maszyna stanów nie ma za zadanie zwiększać liczby plików. Ma sprawić, że po spojrzeniu na model wiadomo, co może wydarzyć się teraz, co jest niemożliwe i jak system zareaguje na błąd. To właśnie ta przewidywalność zwykle daje większą korzyść niż sama elegancja kodu.

FAQ - Najczęstsze pytania

Podstawą są stany, zdarzenia, przejścia i stan początkowy. W większych rozwiązaniach dochodzą akcje, warunki ochronne oraz kontekst przechowujący dane takie jak komunikat błędu, kod HTTP czy liczba prób.

Warto ją rozważyć, gdy proces ma co najmniej trzy lub cztery znaczące stany, operacje asynchroniczne i wiele ścieżek błędu. If/else pasuje do krótkiej lokalnej logiki, a reducer służy głównie do wyliczania nowego stanu, natomiast maszyna stanów jasno określa legalne przejścia.

Stan opisuje tryb procesu, na przykład loading albo success, a kontekst przechowuje dane pomocnicze, takie jak treść błędu lub liczba prób. Funkcja przejścia powinna wyłącznie wyliczać kolejny stan, podczas gdy żądania HTTP, zapis danych i aktualizacja interfejsu powinny być obsługiwane osobno.

Najpierw opisz jeden konkretny proces, wypisz jego stany i zdarzenia oraz rozrysuj dozwolone przejścia. Na początku wystarczy mapa przejść i czysta funkcja, a bibliotekę taką jak XState warto dodać dopiero przy potrzebie hierarchii, równoległości, wizualizacji lub współdzielenia modelu.

Oceń artykuł

Ocena: 5.00 Liczba głosów: 1

Tagi:

formularze płatności maszyny stanów statechart xstate

Udostępnij artykuł

Alex Jabłoński

Alex Jabłoński

Nazywam się Alex Jabłoński i od 14 lat zajmuję się programowaniem webowym. Moja przygoda z tą dziedziną zaczęła się od prostych projektów, które stopniowo przerodziły się w pasję do tworzenia złożonych aplikacji internetowych. Interesuje mnie nie tylko kodowanie, ale także dzielenie się wiedzą, co skłoniło mnie do pisania artykułów, które pomagają innym zrozumieć zasady programowania. W swoich tekstach koncentruję się na podstawach, ale także na bardziej zaawansowanych zagadnieniach, starając się w przystępny sposób wyjaśnić trudne tematy. Przy pisaniu zwracam szczególną uwagę na aktualność informacji oraz ich zrozumiałość. Regularnie śledzę nowinki w branży, porównuję różne podejścia i staram się uprościć skomplikowane koncepcje, aby były dostępne dla każdego, niezależnie od poziomu zaawansowania. Moim celem jest dostarczenie wartościowych treści, które pomogą czytelnikom w ich drodze do kariery w programowaniu webowym.

Napisz komentarz