createMachine: la anatomía de una máquina
Una máquina de estados finitos es la forma más antigua y mejor entendida de modelar comportamiento: un conjunto cerrado de situaciones y las transiciones legales entre ellas. XState toma esa teoría y la vuelve un valor de JavaScript con `createMachine`. Esta lección disecciona la anatomía de una máquina en v5 —`id`, `initial`, `states` y `on`, más el `on` global de la raíz— y demuestra por qué declarar los estados de forma explícita elimina de raíz la clase entera de bugs que nace de combinar banderas booleanas sueltas.
Casi todo bug de estado nace de la misma raíz: representamos una situación con varias banderas booleanas independientes y confiamos —sin garantía— en que jamás aparezca una combinación imposible. cargando y error a true a la vez; enviado sin datos; un formulario a la vez limpio y sucio. Una máquina de estados finitos ataca el problema por debajo: en lugar de banderas que se combinan libremente, declaras un conjunto cerrado y explícito de estados y las únicas transiciones legales entre ellos. createMachine es el constructor de esa declaración en XState v5. No ejecuta nada todavía: produce un valor puro que describe un autómata, y esa descripción es a la vez el modelo mental, la documentación y el código ejecutable.
- Entender la máquina de estados finitos como la quíntupla clásica y por qué prohíbe los estados imposibles.
- Escribir una máquina con
createMachiney sus cuatro piezas esenciales:id,initial,statesyon. - Definir transiciones con
oncomo un mapa de evento a estado destino, local o global. - Distinguir la definición pura de la máquina de su futura ejecución con un actor.
Del booleano al estado explícito
Cuatro banderas booleanas describen dieciséis combinaciones. La mayoría no significan nada: cargando con error, exito con vacio. El código que las usa acaba lleno de condicionales defensivos que intentan reconstruir, a posteriori, cuál de esas dieciséis combinaciones es la “de verdad”. El espacio de estados creció como 2 elevado a n, pero solo un puñado de puntos de ese espacio son legales.
// Cuatro banderas: dieciseis combinaciones, casi todas ilegales
let cargando = false
let error = false
let exito = false
let vacio = false
// nada en el sistema de tipos impide cargando y error a la vez
Una máquina de estados finitos invierte la lógica: no enumera lo que puede combinarse, sino lo que puede ocurrir. Formalmente es una quíntupla —un conjunto de estados, un alfabeto de eventos, una función de transición, un estado inicial y un conjunto de estados finales—. El espacio deja de ser 2 elevado a n y pasa a ser exactamente los estados que tú nombraste. Lo imposible no se comprueba en tiempo de ejecución: es inexpresable.
La pieza que hace el trabajo es la función de transición. En la teoría se escribe como una función que, dado un estado y un símbolo del alfabeto, devuelve el estado siguiente. En una máquina bien diseñada esa función es parcial a propósito: no todos los pares de estado y evento tienen imagen. Cuando un par no está definido, el evento no produce transición, y esa ausencia es una decisión de diseño tan deliberada como cualquier flecha dibujada.
Esa parcialidad deliberada es la diferencia entre permitir y prohibir. Un modelo que solo admite lo declarado es más fácil de razonar que uno que rechaza lo indeseado: el conjunto de lo permitido es finito y está a la vista, mientras que el de lo indeseado es abierto e inagotable. Enumerar lo posible acota el sistema; intentar enumerar lo imposible no termina nunca.
createMachine: las cuatro piezas
createMachine recibe un único objeto de configuración. En su forma mínima aparecen cuatro claves: id, un nombre para la máquina; initial, el estado en el que arranca; states, el mapa de todos los estados posibles; y, dentro de cada estado, on, el mapa de las transiciones que ese estado admite.
import { createMachine } from 'xstate'
const alternador = createMachine({
id: 'alternador',
initial: 'inactivo',
states: {
inactivo: {
on: { ALTERNAR: 'activo' },
},
activo: {
on: { ALTERNAR: 'inactivo' },
},
},
})
Léelo como lo que es: un autómata de dos estados. Arranca en inactivo. Estando inactivo, el evento ALTERNAR lo lleva a activo; estando activo, el mismo evento lo devuelve a inactivo. No hay una tercera situación posible, y no existe ningún evento capaz de crear una. El estado ya no es una conjunción de banderas: es una única variable con un dominio cerrado.
id
El identificador de la máquina. Da nombre a la raíz del árbol y a los estados internos; aparece en las trazas y en el diagrama que verás en la última lección.
initial
El estado por el que arranca. Toda máquina —y todo estado compuesto— debe declarar cuál de sus hijos es el punto de partida.
states
El mapa cerrado de todos los estados posibles. Lo que no está aquí no existe: es la enumeración exhaustiva del comportamiento.
on
Dentro de cada estado, el mapa de eventos a destinos. Es la función de transición restringida a ese estado.
XState v4 aceptaba enviar un evento como una cadena suelta. La v5 lo prohíbe: todo evento es un objeto con una propiedad type obligatoria, como actor.send({ type: 'ALTERNAR' }). La razón es la uniformidad —un evento con carga útil, como { type: 'ESCRIBIR', valor: 'a' }, tiene la misma forma que uno sin ella— y un mejor tipado en TypeScript. En la definición de la máquina, el type del evento es la clave dentro de on.
Las transiciones viven en on
El bloque on de un estado es, literalmente, la función de transición restringida a ese estado: dada la situación actual y un evento, devuelve la situación siguiente. En su forma más corta, cada entrada mapea el type de un evento al nombre del estado destino. Lo revelador es lo que NO aparece: si un estado no declara una entrada on para cierto evento, ese evento simplemente no produce ningún efecto en ese estado. La transición ilegal no se rechaza con un if; es imposible por omisión.
stateDiagram-v2 [*] --> inactivo inactivo --> activo: ALTERNAR activo --> inactivo: ALTERNAR
Esa asimetría es la clave pedagógica de la lección. En el modelo de banderas, para prohibir una transición hay que escribir código que la impida. En el modelo de máquina, para permitir una transición hay que declararla; todo lo no declarado está prohibido por defecto. El diseño pasa de una lista negra frágil a una lista blanca exhaustiva, y esa exhaustividad es la que un revisor humano —o una herramienta— puede auditar de un vistazo.
Y como la declaración es exhaustiva, se puede verificar de forma mecánica: dado el conjunto de estados y el de eventos, comprobar qué pares tienen transición y cuáles no es un recorrido finito. Esa tabla completa es, ni más ni menos, la especificación del comportamiento, y es también lo que permitirá dibujar la máquina y probarla sin ejecutarla en las lecciones siguientes.
Un ejemplo menos de juguete: una petición de datos, el caso que abría la lección con banderas, modelado ahora como cuatro estados entre los que no cabe ninguna combinación.
const datos = createMachine({
id: 'datos',
initial: 'inactivo',
states: {
inactivo: {
on: { CARGAR: 'cargando' },
},
cargando: {
on: {
EXITO: 'exito',
ERROR: 'fallo',
},
},
exito: {
on: { CARGAR: 'cargando' },
},
fallo: {
on: { REINTENTAR: 'cargando' },
},
},
})
Las dieciséis combinaciones del principio se han reducido a cuatro estados nombrados. No existe forma de estar cargando y en fallo a la vez, porque el estado es uno solo; el bug más común de las peticiones asíncronas desaparece por construcción, no por vigilancia. Y el estado cargando solo puede alcanzarse desde inactivo, exito o fallo mediante un evento explícito, nunca por accidente.
Transiciones globales: el on de la máquina
A veces un evento debe atenderse en cualquier estado: una avería, un cierre de sesión, un reinicio de emergencia. Repetir esa transición en cada estado sería ruido y una fuente de olvidos —basta añadir un estado nuevo y olvidar copiarla para abrir un agujero—. Para eso, createMachine admite un bloque on en el nivel superior, hermano de states: las transiciones que declares ahí aplican con independencia del estado en que se encuentre la máquina.
const alternador = createMachine({
id: 'alternador',
initial: 'inactivo',
states: {
inactivo: { on: { ALTERNAR: 'activo' } },
activo: { on: { ALTERNAR: 'inactivo' } },
intermitente: {},
},
on: {
AVERIA: '.intermitente', // desde cualquier estado
},
})
Fíjate en el destino .intermitente, con punto inicial: el punto indica un destino relativo a la raíz de la máquina, es decir, su hijo intermitente. Con esta pieza la anatomía queda completa —id, initial, states y los dos niveles de on, el local de cada estado y el global de la máquina—. El on de nivel superior es la contrapartida de la factorización jerárquica que verás en la lección siguiente: en lugar de subir la transición a un ancestro intermedio, la eleva hasta la raíz, donde alcanza a todo el autómata sin excepción.
Una máquina finita, por definición, solo tiene un número finito de estados. Pero muchos problemas necesitan además datos cuantitativos —un contador, un texto, una lista— que no caben en un conjunto finito de nombres. Para eso la máquina lleva un context: un objeto de datos que acompaña al estado finito y que las acciones actualizan. El estado finito responde a “en qué situación estoy”; el context, a “con qué valores”. La combinación es una máquina de estados extendida, y es lo que lleva a XState más allá de los autómatas de juguete hacia los formularios, las peticiones y los asistentes reales.
La tentación al empezar es tratar createMachine como una forma verbosa de un switch. La lección profunda es la contraria: el acto de enumerar los estados TE OBLIGA a decidir, antes de escribir una sola línea de comportamiento, cuántas situaciones distintas admite tu sistema y cómo se pasa de unas a otras. Ese ejercicio expulsa los estados imposibles no porque los detecte, sino porque nunca les da un nombre; y lo que no tiene nombre no puede ocurrir. Una máquina de estados finitos no es documentación de un comportamiento que ya existe: es la definición del comportamiento, y la interfaz solo puede reflejar los estados que la máquina permite. Cuando interiorizas que initial, states y on son la quíntupla de la teoría de autómatas escrita en JavaScript, dejas de ver XState como una librería de gestión de estado y empiezas a verlo como lo que es —una notación ejecutable para el modelo formal más viejo y mejor entendido de la computación—. El resto del nivel son refinamientos sobre esta idea: jerarquía, finalización, ejecución, inspección. Pero el corazón está aquí: los estados que no nombras son estados que tu programa no puede alcanzar, y las transiciones que no declaras son transiciones que no pueden ocurrir. Diseñar es, exactamente, elegir ese conjunto.
- Escribe con
createMachineun semáforo de tres estados —rojo,verde,amarillo— con un único eventoSIGUIENTEque ciclerojoaverde,verdeaamarilloyamarilloarojo. - Marca
rojocomo estadoinitialy comprueba que ningún otro evento produce transiciones no declaradas. - Enumera cuántas combinaciones tendría el mismo problema modelado con tres banderas booleanas y cuáles serían ilegales.
- Añade un evento
AVERIAque desde cualquier estado lleve a un nuevo estadointermitenteusando el bloqueonde nivel superior de la máquina. - Dibuja el diagrama de estados a mano y verifícalo contra tu definición: cada flecha debe corresponder a una entrada de
on, y cada entrada deona una flecha.