Máquinas de estados finitos: el patrón de diseño más infrautilizado del desarrollo frontend
Ensayo

Máquinas de estados finitos: el patrón de diseño más infrautilizado del desarrollo frontend

Osama Alghanmi · 19 de febrero de 2026

Si usas useState para una interfaz compleja, probablemente lo estás haciendo mal. Hay una solución de hace 50 años que estás ignorando.

La trampa de las banderas booleanas

Este es un patrón conocido:

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! }

Esto crea 2^n estados posibles (32 combinaciones para 5 booleanos). La mayoría son inválidas o absurdas.

La alternativa: la máquina de estados

¿Y si definieras los estados válidos de forma explícita?

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" } ] }

Ahora hay exactamente 5 estados y 9 transiciones válidas. Ninguna combinación imposible.

Visualizar la diferencia

Banderas booleanas: estado espagueti

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

Cualquier combinación es posible. Los errores nacen de estados inválidos que no consideraste.

Máquina de estados: grafo dirigido

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 │ │ └──────────────────────────────┘

Cada camino es explícito. Las transiciones inválidas no existen.

Ejemplo real: envío de un formulario

La forma con booleanos

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? }

La forma con máquina de estados

Define estados y transiciones explícitos:

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" } ] }

Ventajas:

  • ✅ No se puede enviar mientras ya se está enviando
  • ✅ La validación ocurre en su propio estado
  • ✅ El error y el éxito son mutuamente excluyentes
  • ✅ Caminos claros para reintentar

Si usas XState, esto se traduce directamente. Si prefieres algo más ligero, incluso una unión discriminada (discriminated union) simple de TypeScript te lleva la mayor parte del camino:

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

Por qué los desarrolladores evitan las máquinas de estados

Mito 1: «Son demasiado complejas»

Realidad: las banderas booleanas parecen más simples hasta que tienes 5 o más. Entonces la matriz de interacciones se vuelve incomprensible.

Mito 2: «Solo sirven para juegos»

Realidad: los desarrolladores de juegos usan máquinas de estados finitos porque funcionan. La interfaz es igual que un juego: las acciones del usuario provocan cambios de estado.

Mito 3: «Son difíciles de cambiar»

Realidad: cambiar una máquina de estados significa añadir un estado o una transición. Cambiar banderas booleanas significa buscar entre cadenas de useEffect.

Cuándo usar máquinas de estados

EscenarioBanderas booleanasMáquina de estados
2-3 estados simples✅ Aceptable✅ Mejor
Operaciones asíncronas❌ Llenas de errores✅ Claro
Flujos de varios pasos❌ Desordenado✅ Perfecto
Modos de interfaz complejos❌ Imposible✅ Ideal

Una analogía real: los semáforos

Los semáforos son la máquina de estados canónica:

text
1 Red → Green → Yellow → Red

Imagina que los semáforos usaran banderas booleanas:

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!

Los ingenieros de tráfico usan máquinas de estados porque hay vidas que dependen de estados predecibles.

La cordura de tus usuarios también depende de ello.

Pruébalo: convierte un lío de booleanos

Toma este componente cargado de booleanos:

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 }

Conviértelo a estados explícitos:

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

La versión con máquina de estados tiene 6 estados explícitos en lugar de 32 combinaciones booleanas posibles.

La conclusión

Las máquinas de estados finitos no son ejercicios académicos: son herramientas prácticas para gestionar la complejidad.

  • 2-3 booleanos: probablemente bien
  • 4 o más booleanos: considera una máquina de estados
  • Flujos asíncronos: usa una máquina de estados sin dudarlo
  • Interfaz de varios pasos: máquina de estados o nada

Bibliotecas como XState lo hacen fácil en cualquier proyecto de React/Vue/Svelte.

Publicado por primera vez en Dev.to.

Osama Alghanmi

El conocimiento es libertad. Los sistemas deben estar abiertos a las personas a las que gobiernan.

© 2026 Osama Alghanmi. Liubliana, Eslovenia.