
Če za zapleten uporabniški vmesnik uporabljate useState, ga verjetno uporabljate narobe. Obstaja 50 let stara rešitev, ki jo prezirate.
Tole je znan vzorec:
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.
Kaj, če bi veljavna stanja določili izrecno?
{
"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.
isLoading=true
/ \
isError=true? isEditing=true?
/ \
? ?Mogoča je vsaka kombinacija. Hrošči nastanejo zaradi neveljavnih stanj, na katera niste pomislili.
┌─────────┐
┌─────────►│ idle │◄────────┐
│ └────┬────┘ │
│ │ │
ERROR│ FETCH│ SUCCESS
│ ▼ │
┌────┴───┐ ┌─────────┐ │
│ error │ │ loading │ │
└───┬────┘ └────┬────┘ │
▲ │ │
│ SUCCESS │
│ │ │
│ ▼ │
│ ┌─────────┐ │
└───────────┤ editing ├────────┘
└────┬────┘
│ SAVE
▼
┌─────────┐
┌─────────│ saving │─────────┐
│ └─────────┘ │
ERROR│ │SUCCESS
│ │
└──────────────────────────────┘Vsaka pot je izrecna. Neveljavnih prehodov ni.
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?
}Določite izrecna stanja in prehode:
{
"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:
Č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:
type FormState =
| { status: 'editing' }
| { status: 'validating' }
| { status: 'submitting' }
| { status: 'success' }
| { status: 'error'; message: string };Resničnost: logične zastavice videti preprostejše, dokler jih nimate 5 ali več. Potem matrika njihovih medsebojnih vplivov postane neobvladljiva.
Resničnost: razvijalci iger uporabljajo končne avtomate, ker delujejo. Uporabniški vmesnik je podoben igri: dejanja uporabnika sprožajo spremembe stanja.
Resničnost: sprememba avtomata stanj pomeni dodati stanje ali prehod. Sprememba logičnih zastavic pomeni iskanje po verigah useEffect.
| Primer | Logične zastavice | Avtomat 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 |
Semaforji so kanonični avtomat stanj:
Red → Green → Yellow → RedPredstavljajte si, da bi semaforji uporabljali logične zastavice:
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.
Vzemite to komponento, polno logičnih zastavic:
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:
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.
Končni avtomati niso akademske vaje, ampak praktična orodja za obvladovanje zapletenosti.
Knjižnice, kot je XState, to olajšajo v vsakem projektu React/Vue/Svelte.