wandres.dev
XSTATE: EL MODELO · createMachine

Visualizar e inspeccionar: la máquina como diagrama vivo

Porque la máquina es un valor puro y serializable, una herramienta puede dibujarla sin ejecutarla y otra puede animar su ejecución en tiempo real. Esta lección conecta ambas: el visualizador de Stately, que convierte la definición en un diagrama editable de ida y vuelta y permite simular la máquina sin escribir código, y `@statelyai/inspect`, que instrumenta un actor con la opción `inspect` para verlo transicionar en vivo en el navegador. Cierra el bucle del nivel: la definición, el diagrama y la ejecución son tres vistas del mismo artefacto.

⏱ 16 min

La lección anterior estableció que la máquina es un valor puro y serializable. Esta lección cobra la recompensa de esa propiedad. Porque la definición es una estructura de datos y no código opaco, una herramienta puede leerla y dibujarla como un diagrama de estados —sin ejecutar nada— y, aún mejor, puede hacerlo en las dos direcciones: editas el diagrama y obtienes la máquina, o escribes la máquina y obtienes el diagrama. Ese es el visualizador de Stately. Y porque un actor emite eventos observables en cada transición, otra herramienta puede engancharse a un actor vivo y mostrar su estado moviéndose en tiempo real: eso es inspect. Al final del nivel, la definición, el diagrama y la ejecución dejan de ser tres cosas distintas y se revelan como tres vistas del mismo artefacto.

🎯 Al terminar esta lección sabrás
  • Entender por qué la serializabilidad de la máquina es la condición que habilita dibujarla automáticamente.
  • Usar el visualizador de Stately para ver una máquina como diagrama editable y simularla sin código.
  • Instrumentar un actor con la opción inspect y createBrowserInspector para verlo transicionar en vivo.
  • Leer los eventos de inspección —@xstate.actor, @xstate.event, @xstate.snapshot— como sustrato de herramientas propias.

El diagrama es el código

Un diagrama de estados dibujado a mano se desincroniza del código en cuanto alguien toca el código. La propuesta de Stately elimina esa deriva por construcción: como la máquina ES una estructura de datos, el diagrama se genera a partir de ella y no puede mentir. Pegas la definición en el visualizador —disponible en el estudio de Stately en la web y en su extensión para el editor— y obtienes el autómata dibujado, con sus estados, sus transiciones etiquetadas por evento y sus estados finales marcados. Y como el mapeo es biyectivo, puedes arrastrar en el diagrama y recuperar el código correspondiente.

flowchart LR
A[Definicion con createMachine] --> B[Estructura serializable a JSON]
B --> C[Visualizador de Stately]
B --> D[Actor con la opcion inspect]
C -.edicion de ida y vuelta.-> A
D --> E[Diagrama vivo en el navegador]

El estudio ofrece además un modo de simulación: sin arrancar tu aplicación, haces clic sobre los eventos disponibles y el diagrama recorre las transiciones, resaltando el estado activo. Es la máquina ejecutándose en tu cabeza, pero dibujada —una forma de validar el modelo antes de conectarlo a una sola línea de interfaz—. La extensión para el editor mantiene esa vista junto al código: modificas la definición y el diagrama se redibuja al instante, de modo que revisar una máquina en una petición de cambios es leer su diagrama, no descifrar un objeto anidado. Esto reencuadra qué es una máquina de estados: no un diagrama que documenta el código ni un código que implementa un diagrama, sino un único artefacto con dos representaciones equivalentes. El diagrama no es una ayuda visual sobre el código: es el código, mostrado de otra forma.

inspect: ver el actor en vivo

El visualizador dibuja la definición estática y la simula a mano. Para ver la EJECUCIÓN real —qué transición ocurre cuando, con qué evento, con qué contexto— se usa @statelyai/inspect. La idea es instrumentar el actor: createBrowserInspector abre una vista del diagrama en el navegador, y le pasas su función inspect al crear el actor a través de las opciones. A partir de ahí, cada evento que envías se anima sobre el diagrama, resaltando el estado activo y la flecha recorrida.

import { createActor } from 'xstate'
import { createBrowserInspector } from '@statelyai/inspect'
import { maquina } from './maquina'

const { inspect } = createBrowserInspector()

const actor = createActor(maquina, { inspect })
actor.start()

actor.send({ type: 'ENVIAR' })
actor.send({ type: 'OK' })
// cada send se ilumina en el diagrama abierto en el navegador

La opción inspect es un segundo argumento de createActor, junto a otros como input o id. No cambia el comportamiento del actor en absoluto: es una escucha pasiva. Esa es la clave —observar no altera lo observado—, y por eso puedes dejar el inspector activado en desarrollo sin miedo a que modifique la lógica que estás depurando. Si prefieres controlar cuándo se abre la vista, createBrowserInspector acepta opciones como autoStart para no lanzarla hasta que llames a su método start.

💡
El inspector se enchufa por actor, no por máquina

Coherente con la lección anterior: inspect es una opción del ACTOR, no de la máquina, porque solo hay algo que observar cuando algo se ejecuta. La máquina, al ser un valor puro, no emite nada. Puedes inspeccionar unos actores y otros no, cada uno con su propia vista, porque la instrumentación pertenece a la instancia en ejecución y no a la definición compartida.

Los eventos de inspección

createBrowserInspector es una comodidad que dibuja por ti, pero por debajo la opción inspect acepta simplemente una función que recibe eventos de inspección. Ese es el sustrato de bajo nivel sobre el que se construye cualquier herramienta —tu propio logger, un trazador, un panel de devtools—. Los eventos tienen un type con prefijo @xstate.

🎬

@xstate.actor

Se emite cuando un actor nace y se registra. Te da su identidad y su lógica: el punto de partida de cualquier traza.

✉️

@xstate.event

Se emite cuando un actor recibe un evento en su buzón. Lleva el evento completo, con su type y su carga útil.

📸

@xstate.snapshot

Se emite tras cada transición, con el snapshot resultante. Es el latido del actor: cada uno marca un nuevo estado.

const actor = createActor(maquina, {
  inspect: (ev) => {
    if (ev.type === '@xstate.snapshot') {
      console.log('nuevo estado', ev.snapshot.value)
    }
    if (ev.type === '@xstate.event') {
      console.log('evento recibido', ev.event.type)
    }
  },
})

Con esta función tienes acceso programático a toda la historia del actor a medida que ocurre: cada evento entrante y cada snapshot resultante. Escribir un registro de auditoría, reproducir sesiones, alimentar una línea de tiempo de depuración o exportar trazas para un test se reducen a filtrar estos eventos. El inspector visual del navegador no es más que un consumidor bien hecho de exactamente este flujo, y en el servidor un inspector basado en WebSocket consume los mismos eventos sin navegador alguno.

Construir una línea de tiempo, por ejemplo, es acumular esos eventos en un array a medida que llegan:

const linea = []

const actor = createActor(maquina, {
  inspect: (ev) => {
    if (ev.type === '@xstate.event') {
      linea.push({ tipo: 'evento', valor: ev.event.type })
    }
    if (ev.type === '@xstate.snapshot') {
      linea.push({ tipo: 'estado', valor: ev.snapshot.value })
    }
  },
})

Cada entrada de linea es un fotograma del comportamiento: qué llegó y a qué estado condujo. Volcarla a una tabla da la traza completa de una sesión, y compararla contra una traza esperada convierte la inspección en una forma de prueba de extremo a extremo. La misma estructura de datos que dibuja Stately alimenta tus propias herramientas.

ℹ️
La inspección también observa a los hijos

Cuando una máquina invoca a otras —actores hijos, material de niveles posteriores— la opción inspect del actor raíz recibe también sus eventos: cada @xstate.actor de un hijo que nace, cada snapshot de sus transiciones. Por eso el diagrama vivo puede mostrar no solo una máquina sino todo un sistema de actores relacionados, con sus mensajes cruzándose. La inspección es del árbol de actores completo, no de una máquina aislada.

Inspección solo en desarrollo

El inspector es una herramienta de diagnóstico, no de producción: abrir una pestaña con un diagrama no tiene sentido para un usuario final, y el paquete @statelyai/inspect no debería viajar en el código que sirves. El patrón habitual es crear el inspector solo cuando una variable de entorno de desarrollo lo pide, y pasar su inspect —o undefined— al actor. Como inspect es opcional, pasar undefined deja al actor exactamente como estaría sin instrumentar.

import { createActor } from 'xstate'
import { maquina } from './maquina'

let inspect
if (import.meta.env.DEV) {
  const mod = await import('@statelyai/inspect')
  inspect = mod.createBrowserInspector().inspect
}

const actor = createActor(maquina, { inspect })
actor.start()

La importación dinámica dentro de la rama de desarrollo consigue, además, que el empaquetador excluya por completo la librería del build de producción: si el import vive tras una condición que el compilador puede resolver como falsa, el código muerto se poda. El resultado es la mejor de las dos situaciones: observabilidad total mientras desarrollas, cero peso cuando publicas.

La observabilidad no se añade: viene de que el comportamiento es un valor

En la mayoría de los sistemas, hacer que el estado sea observable es un trabajo extra: hay que instrumentar a mano, sembrar logs, exponer hooks. Con una máquina de estados no se añade nada, porque la observabilidad ya estaba implícita en el modelo. La definición es un valor serializable, así que dibujarla es leerla; la ejecución es una secuencia de transiciones explícitas entre estados nombrados, así que observarla es escuchar esas transiciones. El mapa y el territorio coinciden: el diagrama que dibuja Stately y el comportamiento que corre en producción son literalmente la misma estructura, una en reposo y otra en movimiento. Esto es lo que ningún montón de banderas booleanas puede ofrecer —no hay diagrama que dibujar de un if anidado, ni transición que resaltar cuando el estado es un enredo de variables sin nombre—. Al forzarte a nombrar cada estado y cada transición, la máquina construye, como efecto secundario inevitable, un sistema que se explica a sí mismo: visualizable antes de ejecutarse, simulable sin conectarse a nada, inspeccionable mientras se ejecuta, reproducible después. La depurabilidad deja de ser una herramienta que compras y pasa a ser una propiedad que el modelo te regala. Cuando entiendes que el diagrama vivo no es una vista bonita sobre tu código sino tu código mismo, visto en el tiempo, comprendes por qué modelar con estados explícitos paga su coste con creces: has escrito un programa que, por su forma, sabe contar lo que hace.

⚔️ Cierra el bucle
  1. Toma la máquina de pago de la lección 3 y pégala en el visualizador de Stately; comprueba que sus estados finales aparecen marcados y recórrela en modo simulación.
  2. Instala @statelyai/inspect, instrumenta un actor con createBrowserInspector y observa cómo cada send se anima en el diagrama del navegador.
  3. Sustituye el inspector visual por una función inspect propia que registre en consola cada @xstate.snapshot con el nuevo state.value.
  4. Amplía esa función para distinguir también los eventos @xstate.event y construir una línea de tiempo de “evento recibido, estado resultante”.
  5. Envuelve la creación del inspector en una condición de entorno de desarrollo y explica por qué inspect es una opción del actor y no de la máquina.