El coste de una máquina de estado
Adoptar una máquina no es gratis: cobra tres impuestos —verbosidad, curva de aprendizaje y ceremonia— que se pagan por adelantado, mientras que el beneficio crece despacio con el número de estados correlacionados. Esta lección cuantifica ese coste con honestidad, expone el anti-patrón de modelar un toggle como máquina, mide la asimetría entre un coste frontal y un beneficio tardío, y saca a la luz los impuestos invisibles —depurar a través de la abstracción y la falsa sensación de seguridad— que ninguna estimación previa contempla.
La lección anterior fue optimista: mostró dónde la máquina gana. Esta es su contrapeso obligatorio, porque un criterio que solo ve las ventajas no es un criterio, es una moda. Toda máquina de estado tiene una factura, y esa factura tiene una forma peculiar: es alta y se paga por adelantado, mientras que el beneficio llega tarde y crece despacio. De esa asimetría nace el error más común del que acaba de aprender statecharts —modelarlo todo como máquina— y también su cura. Vamos a leer la factura línea por línea, incluidas las partidas que no aparecen en ningún presupuesto y solo se descubren en producción.
- Descomponer el coste de una máquina en tres impuestos concretos: verbosidad, curva y ceremonia.
- Reconocer el anti-patrón de la máquina para lógica trivial y por qué es una pérdida neta.
- Entender la forma del coste —alto y por adelantado— frente a la del beneficio —tardío y creciente—.
- Detectar los impuestos invisibles: depurar la abstracción y la falsa confianza que induce.
Tres impuestos: verbosidad, curva y ceremonia
El primer impuesto es la verbosidad. Lo que como estado local ocupa tres líneas —una bandera y su setter— como máquina ocupa un bloque de configuración con types, states, on y a menudo context, guards y actions. No es un defecto de XState; es intrínseco a hacer explícito un protocolo. El coste es real y hay que contarlo.
Conviene ponerle un número a esa verbosidad, porque el número asusta. Un problema que se resolvía con un useState de tres líneas se convierte, como máquina cableada a la UI, en treinta o cuarenta líneas repartidas entre la definición, el tipado de eventos y el hook de conexión. La relación señal/ruido se desploma: el noventa por ciento del código describe la maquinaria y el diez por ciento, el problema. Cuando la maquinaria compra garantías, esa proporción es una inversión; cuando no hay nada que garantizar, es ruido puro que todo lector futuro tendrá que atravesar para encontrar las tres líneas que importaban.
El segundo impuesto, más caro y más subestimado, es la curva de aprendizaje, y no la tuya: la del equipo. Una máquina introduce un vocabulario —estados, eventos, transiciones, guards, actions, assign, actores, invoke, el modelo SCXML— que todo el que toque ese código debe dominar. Un useState lo entiende cualquiera en cero segundos; una máquina de XState exige que el lector sepa leer statecharts.
Y ese conocimiento no vive en el repositorio: vive en cabezas que rotan, se van y se incorporan. Adoptar una máquina no es una decisión técnica local, es un cambio en el idioma del proyecto. A partir de ese commit, cada persona que abra el archivo necesita saber qué distingue una action de un guard, cuándo corre assign y cómo se lee la jerarquía de un statechart. La curva no se paga una vez; se paga una vez por cada persona, y otra vez cada vez que alguien la olvida. Contar solo tu tiempo de escritura es medir la punta de un iceberg cuyo grueso es ajeno y futuro.
El tercer impuesto es la ceremonia: el andamiaje que rodea a la máquina para conectarla al mundo. Tipar eventos, cablear el actor a la UI con useMachine o useSelector, sincronizar lo que renderizas con el context, persistir y rehidratar el snapshot, enchufar el inspector. Nada de esto es difícil, pero todo suma, y para lógica pequeña la ceremonia puede exceder con holgura a la complejidad que venías a domar.
Los tres impuestos comparten un rasgo que los hace fáciles de subestimar: se pagan en momentos y por personas distintas de quien decide adoptarlos. Quien elige la máquina paga la verbosidad hoy; el equipo paga la curva durante meses; quien depura a medianoche paga la ceremonia. Como el coste está diferido y repartido, el que decide rara vez lo siente entero, y por eso la decisión tiende a sesgarse siempre hacia el sí.
Es tentador decir “la verbosidad no importa, el código se escribe una vez y se lee muchas”. Pero la curva y la ceremonia no son costes de escritura: son costes de lectura y mantenimiento, que se pagan en cada cambio y por cada persona. Descontar ese coste porque “la máquina es la forma correcta” es exactamente el razonamiento que produce sobreingeniería. La forma correcta de un problema trivial también es trivial.
El anti-patrón: una máquina para un toggle
Nada ilustra mejor la factura que el caso donde no debía pagarse. Un interruptor —abierto o cerrado, encendido o apagado— es un booleano. Su lógica cabe en una línea. Modelarlo como máquina es el “hola mundo” de la sobreingeniería: se ve elegante en un tutorial y es un lastre en producción.
import { setup } from 'xstate'
import { useMachine } from '@xstate/react'
// Sobreingenieria: una maquina para lo que es un booleano.
const interruptor = setup({
types: { events: {} as { type: 'TOGGLE' } },
}).createMachine({
id: 'interruptor',
initial: 'apagado',
states: {
apagado: { on: { TOGGLE: 'encendido' } },
encendido: { on: { TOGGLE: 'apagado' } },
},
})
function Interruptor() {
const [snap, send] = useMachine(interruptor)
return <button onClick={() => send({ type: 'TOGGLE' })}>{snap.matches('encendido') ? 'ON' : 'OFF'}</button>
}
// La version honesta: el problema entero, sin dependencias.
function InterruptorSimple() {
const [on, setOn] = useState(false)
return <button onClick={() => setOn((v) => !v)}>{on ? 'ON' : 'OFF'}</button>
}
Las dos hacen lo mismo. La primera cobra los tres impuestos —verbosidad, un concepto que el lector debe conocer y una dependencia— a cambio de cero garantías adicionales, porque un booleano no tiene estados imposibles que prevenir: sus dos valores son ambos legales y la única transición posible es la que ya existe. Pagar el seguro contra un riesgo que no existe es la definición operativa de sobreingeniería. La máquina no está mal escrita; está mal elegida.
El origen del anti-patrón es casi siempre pedagógico. Los tutoriales enseñan máquinas con ejemplos mínimos —un toggle, un semáforo, un contador— porque son fáciles de mostrar en una pantalla, y el lector infiere, sin que nadie se lo diga, que ese es el tamaño de problema para el que la herramienta sirve. Es al revés: esos ejemplos son didácticos precisamente porque no necesitan una máquina, y por eso caben en una diapositiva. La herramienta se justifica en el problema que no cabe en la diapositiva, el que tiene doce estados y treinta transiciones con reglas. Confundir el ejemplo de enseñanza con el caso de uso es el modo más común de importar ceremonia sin traer beneficio.
Y una vez importado, el anti-patrón se reproduce. El siguiente toggle también se modela como máquina para ser consistente con el anterior, y en seis meses el proyecto acumula una docena de máquinas de dos estados que nadie necesitaba, cada una con su archivo, su tipo de eventos y su hook. La sobreingeniería rara vez llega de golpe: se acumula por imitación de un primer caso mal elegido, y desmontarla después cuesta más que no haberla introducido nunca.
Verbosidad
Treinta o más líneas de maquinaria para tres de problema. Aceptable cuando la maquinaria compra garantías; ruinoso cuando no hay nada que garantizar.
Curva
Todo lector debe saber leer statecharts. Es un impuesto sobre el equipo entero, recurrente y sensible a la rotación, no un coste único tuyo.
Ceremonia
Tipos de eventos, cableado a la UI, sincronía del context, persistencia del snapshot, inspector. Andamiaje que para lógica pequeña supera a la lógica.
La forma del coste y el punto de cruce
Lo que hace peligrosa la decisión es la asimetría temporal entre coste y beneficio. El coste de una máquina es en buena parte fijo y frontal: pagas la curva y la ceremonia casi enteras el primer día, tenga tu problema dos estados o veinte. El beneficio, en cambio, es proporcional al número de estados correlacionados y transiciones con reglas que la máquina vuelve imposibles de romper: crece despacio al principio y se dispara cuando el protocolo se complica.
flowchart LR A[complejidad trivial] --> B[coste supera al beneficio] C[complejidad media] --> D[punto de cruce] E[complejidad alta] --> F[beneficio supera al coste] B --> G[usa estado local o enum] D --> H[decision por contexto] F --> I[la maquina se paga sola] style B fill:#f38ba8,color:#11111b style D fill:#f9e2af,color:#11111b style F fill:#a6e3a1,color:#11111b
De ahí sale un test rápido: estima cuántos estados correlacionados y cuántas transiciones con reglas tiene tu problema hoy y en seis meses. Si son pocos y no van a crecer, estás a la izquierda del cruce y la máquina es una pérdida neta; el enum o el estado local ganan. Si son muchos o crecerán, estás a la derecha y la máquina se amortiza. El punto medio —tres o cuatro estados con algunas reglas— es genuinamente ambiguo y lo resolverán otros factores: cuánta gente mantiene el código, si el diseño ya viene como diagrama, si hay asincronía con cancelación. Esos matices son la lección 5.
Merece subrayarse que la asimetría no solo separa escritura de mantenimiento, sino presente de futuro. El coste de la curva y la ceremonia se paga entero el primer día y se repite en cada lectura; el beneficio solo se cobra el día en que un estado imposible que la máquina prohibía habría causado un bug. Si ese día no llega —porque el problema era simple— pagaste un seguro que nunca cubrió nada. La pregunta honesta, entonces, no es “¿es correcta la forma de máquina?” sino “¿es probable que el seguro se cobre?”, y esa probabilidad es proporcional a cuántas transiciones ilegales acecharían sin la máquina.
La ceremonia, además, no es una cifra fija: crece con las exigencias del entorno. La misma máquina que en un prototipo son diez líneas, en producción arrastra rehidratación, inspección y sincronía, y cada servicio que le enchufas es andamiaje que hay que mantener.
// La ceremonia real de produccion: tipar no basta, hay que cablear.
const actor = createActor(maquina, {
snapshot: cargarSnapshot(), // rehidratar el estado persistido
inspect: (ev) => inspector.send(ev), // conectar el visualizador
})
actor.subscribe((snap) => guardarSnapshot(snap)) // persistir en cada transicion
actor.start()
Cada una de esas líneas es correcta y necesaria en un sistema serio; el punto es que ninguna existe en un useState y todas hay que escribirlas, entenderlas y mantenerlas aunque el estado que gobiernan sea trivial. La ceremonia es el impuesto que peor escala hacia abajo: no se encoge cuando el problema se encoge.
Hay un coste más, difícil de graficar: el lock-in conceptual. Una vez que un flujo es una máquina de XState, refactorizarlo fuera cuesta, y el equipo tiende a modelar los flujos vecinos igual “por consistencia”, extendiendo el impuesto a zonas que no lo necesitaban. La consistencia es una virtud, pero consistencia en la herramienta equivocada solo propaga el coste. La disciplina es elegir la herramienta por problema, no por inercia.
El impuesto invisible: depurar a través de la abstracción
Hay un coste que no aparece en ninguna estimación previa y solo se descubre a las tres de la mañana: cuando algo va mal, ya no depuras tu lógica, depuras la interacción entre tu lógica y la máquina. El bug deja de ser “la variable vale lo que no debía” y pasa a ser “el actor está en un estado que no esperaba porque un evento llegó en un orden que no contemplé, o un guard devolvió falso por un context desincronizado”. La máquina, que te protegía de los estados imposibles, añade una capa que también hay que entender para arreglar los estados posibles. En un problema complejo ese coste está más que justificado, porque la alternativa —depurar veinte booleanos correlacionados— es peor. En uno trivial es puro peaje: pagas por atravesar una abstracción que no necesitabas.
A ese peaje se suma un espejismo peligroso: la máquina da una sensación de seguridad que puede exceder a la seguridad real. Como los estados imposibles del dominio modelado son irrepresentables, es fácil creer que el flujo entero es correcto, cuando lo único garantizado es que las transiciones que escribiste son las que escribiste. Un modelo equivocado —un estado que falta, una arista que sobra— produce una máquina perfectamente consistente y perfectamente incorrecta. El coste de esa falsa confianza no está en las líneas de más, sino en la vigilancia de menos que induce.
Las devtools de XState y su visualizador alivian el primero de estos peajes —hacen visible el estado del actor y la traza de eventos—, pero no eliminan el segundo: ninguna herramienta puede decirte que tu modelo del dominio es el correcto, solo que tu máquina es consistente con el modelo que escribiste. La falsa confianza es un coste del pensamiento, no de la ejecución, y por eso ninguna mejora de tooling la borra.
Si tras leer la factura decides pagarla, págala bien: aísla la máquina de la UI para poder testearla sola, documenta su statechart junto al código para amortizar la curva, y mantén los eventos fuertemente tipados para que el compilador cace las transiciones que olvidaste. Esos tres hábitos no reducen el coste, pero convierten un gasto disperso en una inversión localizada, que es la única forma de que la máquina rente a lo largo del tiempo.
Antes de adoptar, pregúntate también cuánto costará salir. Una máquina bien aislada se reemplaza por un reducer en una tarde; una entrelazada con la UI, los efectos y la navegación se vuelve infraestructura, y quitarla pasa a ser un proyecto. El coste de adopción se estima de antemano; el de salida se sufre por sorpresa. Diseñar la máquina como una caja negra —eventos de entrada, estado de salida— es lo que mantiene esa puerta abierta.
Es cómodo despreciar la máquina-para-un-toggle como un error de novato, pero su raíz es más sutil y más universal. Quien acaba de aprender statecharts no está siendo tonto: está aplicando con entusiasmo una herramienta que de verdad resuelve una clase de problemas, solo que a un problema que no pertenece a esa clase. La sobreingeniería casi nunca es una mala respuesta; es una respuesta correcta a una pregunta que nadie hizo. El toggle no preguntaba “¿cómo prevengo estados imposibles?” porque no los tiene; preguntaba “¿cómo invierto un booleano?”, y a esa pregunta la máquina responde con una parrafada. El coste, entonces, no es un defecto de la herramienta sino la penalización por usarla fuera de su dominio, y esa penalización tiene una forma característica que conviene grabarse: alta y por adelantado, mientras el beneficio es tardío y creciente. Toda decisión de adoptar una máquina es, en el fondo, una apuesta sobre esa curva: apuestas a que tu problema vive a la derecha del cruce, hoy o pronto. El ingeniero maduro no es el que conoce XState, sino el que sabe leer su propia factura antes de firmarla, y tiene el estómago de escribir useState cuando el problema, por humilde que parezca en un tutorial, no daba para más.
- Busca en tu código una máquina de estado —propia o de una librería— que gobierne dos o tres estados sin transiciones con reglas. Reescríbela como
useStateo como un enum y cuenta las líneas que desaparecen. - Para una máquina que sí conserves, enumera los tres impuestos: cuántas líneas de maquinaria, qué conceptos exige del lector, cuánto andamiaje de cableado. Ponle un número a cada uno.
- Estima los estados correlacionados y las transiciones con reglas de ese mismo problema hoy y dentro de seis meses. Sitúalo en el diagrama del cruce.
- Encuentra un flujo que se modeló como máquina “por consistencia” con un vecino. Pregúntate si el problema lo pedía o si solo heredó el impuesto.
- Escribe en una frase la apuesta implícita de cada máquina de tu proyecto: “apuesto a que este problema vive a la derecha del cruce porque…”. Si no puedes completar la frase, tienes una candidata a simplificar.