Končni avtomati: najbolj spregledan načrtovalski vzorec v razvoju uporabniških vmesnikov
Esej

Končni avtomati: najbolj spregledan načrtovalski vzorec v razvoju uporabniških vmesnikov

Osama Alghanmi · 19. februar 2026

Če za zapleten uporabniški vmesnik uporabljate useState, ga verjetno uporabljate narobe. Obstaja 50 let stara rešitev, ki jo prezirate.

Past logičnih zastavic

Tole je znan vzorec:

typescript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function UserProfile() { const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); const [isEditing, setIsEditing] = useState(false); const [isSaving, setIsSaving] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const handleSave = async () => { setIsSaving(true); setIsError(false); try { await saveUser(user); setIsEditing(false); } catch (e) { setIsError(true); setErrorMessage(e.message); } finally { setIsSaving(false); } }; // What combinations are valid? // isLoading=true, isError=true? // isEditing=true, isSaving=true? // Who knows! }

Tako nastane 2^n možnih stanj (32 kombinacij za 5 logičnih vrednosti). Večina jih je neveljavnih ali nesmiselnih.

Alternativa: avtomat stanj

Kaj, če bi veljavna stanja določili izrecno?

json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { "states": [ { "name": "idle", "isInitial": true }, { "name": "loading" }, { "name": "editing" }, { "name": "saving" }, { "name": "error" } ], "events": ["FETCH", "EDIT", "SAVE", "SUCCESS", "ERROR", "CANCEL"], "transitions": [ { "from": "idle", "to": "loading", "event": "FETCH" }, { "from": "loading", "to": "idle", "event": "SUCCESS" }, { "from": "loading", "to": "error", "event": "ERROR" }, { "from": "idle", "to": "editing", "event": "EDIT" }, { "from": "editing", "to": "saving", "event": "SAVE" }, { "from": "saving", "to": "idle", "event": "SUCCESS" }, { "from": "saving", "to": "error", "event": "ERROR" }, { "from": "editing", "to": "idle", "event": "CANCEL" }, { "from": "error", "to": "idle", "event": "CANCEL" } ] }

Zdaj je natanko 5 stanj in 9 veljavnih prehodov. Nobenih nemogočih kombinacij.

Razlika na sliki

Logične zastavice: špageti stanja

text
1 2 3 4 5 isLoading=true / \ isError=true? isEditing=true? / \ ? ?

Mogoča je vsaka kombinacija. Hrošči nastanejo zaradi neveljavnih stanj, na katera niste pomislili.

Avtomat stanj: usmerjen graf

text
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ┌─────────┐ ┌─────────►│ idle │◄────────┐ │ └────┬────┘ │ │ │ │ ERROR│ FETCH│ SUCCESS │ ▼ │ ┌────┴───┐ ┌─────────┐ │ │ error │ │ loading │ │ └───┬────┘ └────┬────┘ │ ▲ │ │ │ SUCCESS │ │ │ │ │ ▼ │ │ ┌─────────┐ │ └───────────┤ editing ├────────┘ └────┬────┘ │ SAVE ▼ ┌─────────┐ ┌─────────│ saving │─────────┐ │ └─────────┘ │ ERROR│ │SUCCESS │ │ └──────────────────────────────┘

Vsaka pot je izrecna. Neveljavnih prehodov ni.

Primer iz prakse: oddaja obrazca

Pot z logičnimi zastavicami

typescript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function ContactForm() { const [formData, setFormData] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); const [isSuccess, setIsSuccess] = useState(false); const [isError, setIsError] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const submit = async () => { setIsSubmitting(true); setIsError(false); setIsSuccess(false); try { await api.submit(formData); setIsSuccess(true); } catch (e) { setIsError(true); setErrorMessage(e.message); } finally { setIsSubmitting(false); } }; // Bug: What if isSuccess and isError are both true? // Bug: Can I submit again while isSubmitting? // Bug: What clears isSuccess? }

Pot z avtomatom stanj

Določite izrecna stanja in prehode:

json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 { "states": [ { "name": "editing", "isInitial": true }, { "name": "validating" }, { "name": "submitting" }, { "name": "success", "isTerminal": true }, { "name": "error" } ], "events": ["SUBMIT", "VALIDATED", "SUCCESS", "FAILURE", "RETRY", "EDIT"], "transitions": [ { "from": "editing", "to": "validating", "event": "SUBMIT" }, { "from": "validating", "to": "submitting", "event": "VALIDATED", "guard": ["=", "@validation.valid", true] }, { "from": "validating", "to": "editing", "event": "VALIDATED", "guard": ["=", "@validation.valid", false] }, { "from": "submitting", "to": "success", "event": "SUCCESS" }, { "from": "submitting", "to": "error", "event": "FAILURE" }, { "from": "error", "to": "editing", "event": "RETRY" } ] }

Prednosti:

  • ✅ Obrazca ni mogoče oddati, medtem ko se že oddaja
  • ✅ Preverjanje poteka v svojem stanju
  • ✅ Napaka in uspeh se izključujeta
  • ✅ Jasne poti za ponoven poskus

Če uporabljate XState, se to preslika neposredno. Če imate raje lažji pristop, vas že navadna razlikovalna unija (discriminated union) v TypeScriptu pripelje večino poti:

typescript
1 2 3 4 5 6 type FormState = | { status: 'editing' } | { status: 'validating' } | { status: 'submitting' } | { status: 'success' } | { status: 'error'; message: string };

Zakaj se razvijalci izogibajo avtomatom stanj

Mit 1: »Preveč so zapleteni«

Resničnost: logične zastavice videti preprostejše, dokler jih nimate 5 ali več. Potem matrika njihovih medsebojnih vplivov postane neobvladljiva.

Mit 2: »Samo za igre so«

Resničnost: razvijalci iger uporabljajo končne avtomate, ker delujejo. Uporabniški vmesnik je podoben igri: dejanja uporabnika sprožajo spremembe stanja.

Mit 3: »Težko jih je spreminjati«

Resničnost: sprememba avtomata stanj pomeni dodati stanje ali prehod. Sprememba logičnih zastavic pomeni iskanje po verigah useEffect.

Kdaj uporabiti avtomate stanj

PrimerLogične zastaviceAvtomat stanj
2-3 preprosta stanja✅ V redu✅ Boljše
Asinhrone operacije❌ Polno hroščev✅ Jasno
Večstopenjski tokovi❌ Neurejeno✅ Odlično
Zapleteni načini vmesnika❌ Nemogoče✅ Idealno

Primerjava iz vsakdana: semaforji

Semaforji so kanonični avtomat stanj:

text
1 Red → Green → Yellow → Red

Predstavljajte si, da bi semaforji uporabljali logične zastavice:

javascript
1 2 3 4 5 6 7 const [isRed, setIsRed] = useState(true); const [isGreen, setIsGreen] = useState(false); const [isYellow, setIsYellow] = useState(false); // Bug: All could be true! // Bug: All could be false! // Bug: Green could turn directly to Red!

Prometni inženirji uporabljajo avtomate stanj, ker življenja so odvisna od predvidljivih stanj.

Razum vaših uporabnikov je odvisen od tega prav tako.

Poskusite: pretvorite zmešnjavo logičnih zastavic

Vzemite to komponento, polno logičnih zastavic:

typescript
1 2 3 4 5 6 7 8 function Checkout() { const [isCartOpen, setIsCartOpen] = useState(false); const [isCheckingOut, setIsCheckingOut] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const [isComplete, setIsComplete] = useState(false); const [hasError, setHasError] = useState(false); // ... nightmare of useEffect }

Pretvorite jo v izrecna stanja:

text
1 2 states: browsing → cartOpen → checkoutForm → processing → complete ↘ error → checkoutForm (retry)

Različica z avtomatom stanj ima 6 izrecnih stanj namesto 32 možnih kombinacij logičnih zastavic.

Bistvo

Končni avtomati niso akademske vaje, ampak praktična orodja za obvladovanje zapletenosti.

  • 2-3 logične zastavice: verjetno v redu
  • 4 ali več logičnih zastavic: razmislite o avtomatu stanj
  • Asinhroni tokovi: vsekakor uporabite avtomat stanj
  • Večstopenjski vmesnik: avtomat stanj ali nič

Knjižnice, kot je XState, to olajšajo v vsakem projektu React/Vue/Svelte.

Prvič objavljeno na Dev.to.

Osama Alghanmi

Znanje je svoboda. Sistemi morajo biti odprti ljudem, ki jim vladajo.

© 2026 Osama Alghanmi. Ljubljana, Slovenija.