wandres.dev
XSTATE: EL MODELO · createMachine

Estados finales y onDone: cuándo termina una máquina

No todo autómata corre para siempre. La quíntupla clásica incluye un conjunto de estados finales, y XState lo materializa con `type: 'final'`. Esta lección explica qué le ocurre a un actor cuando alcanza un estado final —se detiene y su estado pasa a `done`—, cómo un estado compuesto que termina dispara la transición `onDone` de su padre, cómo un estado paralelo confluye, y cómo una máquina produce un `output` al finalizar. Aquí la máquina deja de ser un bucle perpetuo y se convierte en una unidad componible con principio y fin.

⏱ 16 min

Un interruptor no termina nunca: alterna indefinidamente. Pero muchos procesos sí tienen un final —un pago se completa, un asistente de varios pasos concluye, una descarga acaba—. La teoría de autómatas siempre lo contempló: la quíntupla incluye un conjunto de estados finales, los estados en los que la máquina acepta y se detiene. XState lo expresa marcando un estado con type: 'final'. Cuando la máquina raíz alcanza un estado final, su actor se detiene: no procesará más eventos, su ciclo de vida ha concluido. Y cuando el que termina es un estado compuesto, su padre puede reaccionar con onDone. Esa capacidad de terminar y de avisar de que se ha terminado es la que convierte una máquina en una pieza componible dentro de otra mayor.

🎯 Al terminar esta lección sabrás
  • Marcar un estado como terminal con type: 'final' y entender que no admite transiciones de salida.
  • Saber qué le ocurre a un actor cuando alcanza un estado final de nivel raíz: su status pasa a done.
  • Usar onDone en un estado compuesto para transicionar cuando uno de sus hijos llega a un final.
  • Producir un valor de resultado con output y leerlo desde el snapshot del actor terminado.

El estado final: F en la quíntupla

Un estado type: 'final' es un estado sin salida: no declara on porque, una vez dentro, no hay a dónde ir. Es la letra F de la quíntupla —el conjunto de estados de aceptación— hecha configuración. Cuando el estado final está en el nivel raíz de la máquina, alcanzarlo significa que el autómata completo ha terminado.

import { createMachine } from 'xstate'

const pago = createMachine({
  id: 'pago',
  initial: 'formulario',
  states: {
    formulario: {
      on: { ENVIAR: 'procesando' },
    },
    procesando: {
      on: { OK: 'completado', FALLO: 'formulario' },
    },
    completado: {
      type: 'final',
    },
  },
})

Cuando el actor de esta máquina llega a completado, deja de estar activo. Enviarle más eventos no produce nada: la máquina ha aceptado su entrada y se ha detenido. Modelar el final de forma explícita es tan valioso como modelar los estados intermedios, porque distingue “esperando” de “terminado”, dos situaciones que las banderas booleanas suelen confundir en un mismo hecho = true. En el diagrama, un estado final se dibuja distinto —con doble contorno—, de modo que la terminación salta a la vista tanto en el código como en su representación visual.

stateDiagram-v2
[*] --> formulario
formulario --> procesando: ENVIAR
procesando --> completado: OK
procesando --> formulario: FALLO
completado --> [*]

Hay una lectura teórica que ilumina el concepto. En la teoría de autómatas, los estados finales son los estados de aceptación: la máquina lee una secuencia de símbolos y se dice que la ACEPTA si termina en uno de ellos. Traducido a XState, la secuencia de eventos que llevó al actor a un estado final es una secuencia aceptada —una historia válida y completa del proceso—. Un pago que termina en completado es una secuencia de eventos que el modelo reconoce como un pago bien formado, y esa es, literalmente, la noción de aceptación de la que hablan los cursos de lenguajes formales.

Esta lectura explica también por qué un estado final no tiene on: aceptar es detenerse. Si un estado de aceptación pudiera seguir transicionando, no marcaría el final de nada; sería un estado intermedio más. La ausencia de salidas no es una limitación que soportamos, es la definición misma de terminar.

onDone: reaccionar a que un subproceso termine

El estado final cobra todo su poder dentro de un estado compuesto. Cuando la submáquina de un estado compuesto alcanza SU estado final, ese estado compuesto se considera “hecho”, y eso dispara una transición especial declarada en el padre: onDone. Es el mecanismo por el que un subproceso completo le comunica a su contenedor que ha concluido, sin que el contenedor tenga que vigilar los detalles internos.

const flujo = createMachine({
  id: 'flujo',
  initial: 'trabajando',
  states: {
    trabajando: {
      initial: 'paso1',
      states: {
        paso1: { on: { SIGUIENTE: 'paso2' } },
        paso2: { on: { SIGUIENTE: 'fin' } },
        fin: { type: 'final' },
      },
      onDone: 'terminado',
    },
    terminado: {
      type: 'final',
    },
  },
})

Cuando el subestado fin se alcanza, el estado compuesto trabajando termina y su onDone transiciona a terminado. El padre no escucha ningún evento externo: reacciona a la finalización de su hijo. Esto es composición estructural —una máquina que espera a que otra, contenida en ella, acabe—, y es la semilla del patrón de máquinas invocadas que verás más adelante, donde el hijo es una máquina entera ejecutándose como actor propio y su onDone entrega, además, el resultado que produjo.

🏁

type: final

Un estado sin salida. En la raíz, alcanzarlo detiene la máquina; dentro de un compuesto, lo marca como terminado.

📤

output

El valor de retorno de la máquina al terminar. Una función del contexto que produce el resultado del cómputo.

🔔

onDone

La transición del padre que se dispara cuando su submáquina llega a un final. El hijo acaba, el padre sigue.

💡
onDone no es un evento que puedas enviar

onDone responde a un evento sintético que el motor emite automáticamente cuando un estado compuesto alcanza su final: internamente tiene un type con la forma done.state.<id>. No lo envíes tú con send; se dispara solo. Por eso no aparece bajo on sino como una clave propia del estado compuesto. La misma idea reaparecerá con onDone sobre actores invocados, donde el evento sintético lleva además el output del hijo como carga útil.

El actor terminado y su output

Un estado final puede producir un valor de salida. La máquina declara output —una función que recibe el contexto y devuelve el resultado— y ese valor queda disponible en el snapshot cuando el actor termina. El status del snapshot es la señal canónica del ciclo de vida: vale active mientras corre y pasa a done al alcanzar un final de raíz.

import { createActor, createMachine } from 'xstate'

const pedido = createMachine({
  id: 'pedido',
  initial: 'abierto',
  context: { total: 42 },
  states: {
    abierto: { on: { CONFIRMAR: 'cerrado' } },
    cerrado: { type: 'final' },
  },
  output: ({ context }) => ({ total: context.total }),
})

const actor = createActor(pedido).start()
actor.send({ type: 'CONFIRMAR' })

const snapshot = actor.getSnapshot()
snapshot.status   // 'done'
snapshot.output   // { total: 42 }

La distinción entre active y done no es cosmética: es la diferencia entre una máquina que aún puede cambiar y una cuyo comportamiento ha quedado sellado. Un actor done es inmutable de hecho —ya no responde a eventos— y su output es su valor de retorno, exactamente como una función que ha retornado. Modelar el final permite tratar toda una máquina como un cómputo con resultado, no solo como un bucle reactivo, y ese resultado es lo que la máquina de arriba recibirá cuando la invoque.

De hecho, un actor con final puede tratarse como una promesa. En v5, toPromise(actor) devuelve una promesa que se resuelve con el output en cuanto el actor llega a done. La equivalencia es exacta —una máquina con estado final es un cómputo asíncrono con resultado—, y por eso puedes esperar sobre una máquina con await igual que sobre cualquier otra tarea, integrándola en código asíncrono sin ceremonia. Una máquina que nunca termina no tiene promesa asociada, porque una promesa sin resolución posible no significaría nada.

📝
Un estado paralelo termina cuando todas sus regiones terminan

La finalización se compone también con el paralelismo. Un estado type: 'parallel' se considera terminado —y dispara su onDone— solo cuando CADA una de sus regiones ha alcanzado su propio estado final. Es la conjunción lógica de las finalizaciones: el subproceso completo acaba cuando acaban todas sus ramas simultáneas. Así, onDone sirve tanto para secuencias, donde un paso sigue a otro, como para trabajos paralelos que se lanzan a la vez y deben confluir antes de que el sistema continúe.

Terminar es lo que hace componible a una máquina

Una máquina que nunca termina solo puede observarse; una máquina que termina puede COMPONERSE. Ese es el salto conceptual del estado final. Mientras un autómata es un bucle perpetuo, lo único que puedes hacer con él es suscribirte a sus cambios. Pero en cuanto tiene un final y un output, se comporta como una función: recibe eventos como argumentos a lo largo del tiempo, y al terminar devuelve un valor. Y las funciones se componen —una llama a otra, espera su resultado y continúa—. Eso es precisamente lo que hace onDone: convierte “mi hijo terminó y me dio este valor” en una transición del padre, que es la versión temporal y reactiva de “la función interna retornó y sigo con su resultado”. Por eso el conjunto F de la quíntupla, que en los cursos de teoría parece el detalle menos interesante —el que decide si una cadena se acepta—, es en la práctica el que habilita toda la arquitectura de sistemas grandes: máquinas que invocan máquinas que invocan máquinas, cada una terminando y entregando su resultado a la de arriba. Sin estados finales, XState sería una forma elegante de dibujar bucles. Con ellos, es un modelo de procesos jerárquicos que empiezan, calculan y acaban. La finalización no es el borde de la máquina: es su interfaz de retorno, y una interfaz de retorno es lo único que hace falta para que algo se componga con otra cosa.

⚔️ Modela un proceso con final
  1. Escribe una máquina de descarga con estados inactivo, descargando, completado y fallido, y marca completado y fallido como type: 'final'.
  2. Crea un actor, llévalo hasta completado y comprueba que su status es done y que enviarle más eventos no lo cambia.
  3. Envuelve toda la descarga dentro de un estado compuesto tarea y usa onDone para pasar a un estado notificado cuando la descarga interna termine.
  4. Añade un output a la máquina que devuelva el número de bytes descargados desde el contexto y léelo en el snapshot final.
  5. Explica con tus palabras por qué onDone no se envía con send, de dónde sale el evento sintético que lo dispara, y cuándo se dispararía el onDone de un estado paralelo.