wandres.dev
XSTATE: EL ACTOR MODEL · actores y mensajes

spawn y spawnChild: crear actores y su ciclo de vida

El segundo verbo de Hewitt —un actor puede crear otros actores— se materializa en XState v5 en dos herramientas con semánticas distintas. spawn, disponible dentro de assign, crea un hijo y te devuelve su referencia para guardarla en el context. spawnChild es una acción de crear y olvidar: el hijo queda gestionado por un id, sin referencia en el context. Esta lección contrasta ambas, las diferencia de invoke, y recorre el ciclo de vida completo de un actor hijo: creado, activo, detenido, con stopChild y las opciones input y systemId.

⏱ 17 min

Un actor que no puede crear otros actores es una isla estéril. La segunda capacidad del modelo de Hewitt —engendrar hijos— es la que convierte un actor solitario en un sistema. XState v5 te da dos maneras de hacerlo, y elegir bien entre ellas define la claridad de tu diseño: spawn cuando necesitas conservar la dirección del hijo para hablarle después, y spawnChild cuando lo lanzas y lo gestionas por su nombre. Detrás de ambas hay un ciclo de vida que debes controlar, porque un actor que nadie detiene es una fuga de memoria que respira.

🎯 Al terminar esta lección sabrás
  • Usar spawn dentro de assign para crear un hijo y guardar su referencia en el context.
  • Usar spawnChild para crear un hijo gestionado por id, sin referencia en el context.
  • Distinguir spawn y spawnChild de invoke, cuya vida se ata a un estado.
  • Recorrer el ciclo de vida: creado, activo, detenido; y detener con stopChild.

spawn: un hijo con referencia en el context

spawn vive dentro de la acción assign, y por una buena razón: crear el hijo y guardar su dirección son un solo acto. La función que le pasas a assign recibe un objeto con la utilidad spawn; al invocarla, XState instancia y arranca el actor hijo, y te devuelve su referencia —un ActorRef— para que la almacenes en el context. Desde ese momento tienes una dirección a la que enviar mensajes.

import { setup, assign, fromPromise } from "xstate"

const maquina = setup({
  actors: {
    cargador: fromPromise(async () => (await fetch("/datos")).json()),
  },
}).createMachine({
  context: { ref: null },
  on: {
    INICIAR: {
      actions: assign({
        // spawn crea el hijo, lo arranca y devuelve su ActorRef.
        ref: ({ spawn }) => spawn("cargador", { id: "carga1" }),
      }),
    },
  },
})

Guardar la referencia en el context es lo que te permitirá, en la próxima lección, enviarle eventos con sendTo. La regla mental es directa: si vas a necesitar hablarle al hijo más tarde, usa spawn y quédate con su dirección. Fíjate en setup: registrar la lógica del hijo por nombre en el bloque actors es la forma idiomática de la v5, porque desacopla el plano de su uso y da tipos precisos a spawn.

spawnChild: un hijo por id

A veces creas un actor que hace su trabajo por su cuenta y no necesitas conservar su referencia en el estado —un temporizador, un registrador de eventos, un sincronizador de fondo—. Para eso está spawnChild: una acción de crear y olvidar. El hijo existe y corre, pero se identifica por un id en lugar de por una referencia guardada en el context.

import { setup, spawnChild, stopChild, fromCallback } from "xstate"

const maquina = setup({
  actors: {
    reloj: fromCallback(({ sendBack }) => {
      const t = setInterval(() => sendBack({ type: "TIC" }), 1000)
      return () => clearInterval(t)
    }),
  },
}).createMachine({
  // Crear y olvidar: sin referencia en el context, solo un id.
  entry: spawnChild("reloj", { id: "reloj1" }),
  on: {
    PARAR: { actions: stopChild("reloj1") },   // lo detienes por su id
  },
})

La elección entre las dos herramientas no es de estilo, sino de intención. spawn dice “voy a mantener una conversación con este actor”. spawnChild dice “este actor tiene una tarea y se gestiona por su nombre”. Poner en el context una referencia que nunca vas a usar es ruido; lanzar con spawnChild algo a lo que necesitarás responder con precisión es perder su dirección antes de necesitarla.

Un supervisor real combina las dos herramientas: engendra a sus trabajadores al entrar, guarda las referencias que necesitará y los detiene explícitamente al terminar. Este patrón —crear al entrar, detener al salir— es el esqueleto de casi todo actor padre.

import { setup, assign, stopChild } from "xstate"

const supervisor = setup({
  actors: { trabajador },
}).createMachine({
  context: { refs: [] as ActorRef[] },
  initial: "activo",
  states: {
    activo: {
      entry: assign({
        refs: ({ spawn }) => [
          spawn("trabajador", { id: "t1" }),
          spawn("trabajador", { id: "t2" }),
        ],
      }),
      on: { TERMINAR: "cerrado" },
    },
    cerrado: {
      entry: [stopChild("t1"), stopChild("t2")],   // limpieza explicita
      type: "final",
    },
  },
})
⚠️
Un actor que nadie detiene es una fuga

Los actores hijos no desaparecen solos por dejar de mencionarlos. Un fromCallback con un setInterval, un websocket abierto o una suscripción siguen consumiendo recursos hasta que alguien los detiene. Si creaste con spawn, deténlo pasando su referencia o su id a stopChild; si creaste con spawnChild, deténlo por su id. La regla del nivel 13 de SolidJS vuelve aquí con otra ropa: quien crea un recurso de larga vida es responsable de su destrucción. Un actor huérfano y vivo es la fuga de memoria más difícil de ver, porque el código que lo creó ya ni lo nombra.

El ciclo de vida y su final

Todo actor recorre el mismo arco. Nace creado, pasa a activo cuando arranca —al instanciarlo con createActor y llamar a start, o en el instante en que un padre lo engendra con spawn—, procesa eventos mientras vive, y termina detenido. Un actor se detiene por tres vías: porque su lógica llega a un estado final y concluye por sí mismo, porque falla con un error, o porque alguien lo detiene explícitamente con stopChild. Al detenerse, ejecuta su limpieza —esa función que devolvías en fromCallback— y libera lo que retenía.

flowchart LR
A[creado] -- start o spawn --> B[activo]
B -- procesa eventos --> B
B -- final, error o stopChild --> C[detenido]
style A fill:#f9e2af,color:#11111b
style B fill:#a6e3a1,color:#11111b
style C fill:#eba0ac,color:#11111b
🥚

Creado

El actor existe como instancia pero aún no procesa eventos. Dura un instante: al arrancar pasa a activo.

🏃

Activo

Tiene un buzón vivo, procesa eventos de uno en uno y puede engendrar hijos. Es donde transcurre casi toda su vida.

⚰️

Detenido

Ejecutó su limpieza y liberó sus recursos. No se reanuda: un actor detenido es historia, no una pausa.

Conviene contrastar spawn y spawnChild con la tercera vía de crear actores, invoke, que verás a fondo en el nivel 14. invoke ata la vida del actor a un estado concreto de la máquina: el actor nace al entrar en ese estado y se detiene automáticamente al salir. Es declarativo y perfecto cuando la existencia del hijo coincide con la permanencia en un estado —cargar datos mientras estás en cargando—. spawn y spawnChild, en cambio, son imperativos: creas el hijo cuando lo decides con una acción, y vive hasta que tú u otro lo detengáis, o hasta que su padre muera y se lo lleve consigo.

Vía Cómo se crea Cuándo muere Referencia
spawn Imperativa, dentro de assign Al detenerlo o al morir el padre En el context
spawnChild Imperativa, como acción Al detenerlo por id o morir el padre Por id, no en el context
invoke Declarativa, ligada a un estado Al salir de ese estado Gestionada por la máquina
ℹ️
input y systemId: dos opciones al crear

Al crear un actor, tanto spawn como spawnChild aceptan opciones. input le pasa datos iniciales de solo lectura al hijo —el equivalente a los argumentos de un constructor— que este lee al arrancar para configurar su context. systemId registra al hijo bajo un nombre global dentro del sistema de actores, de modo que cualquier otro actor pueda encontrarlo sin tener su referencia; es el puente hacia la lección 5. El id local identifica al hijo ante su padre; el systemId lo hace descubrible en todo el sistema.

spawn es el segundo axioma de Hewitt vestido de assign

En la primera lección, crear actores era uno de los tres verbos abstractos del modelo. Aquí lo ves encarnado, y con un matiz que la teoría no obligaba pero la práctica exige: la gestión del ciclo de vida. Hewitt describió un mundo ideal donde los actores nacen y persisten; una aplicación real corre en un navegador con memoria finita, y por eso XState acopla la creación a la destrucción. spawn no es solo “haz un actor”: es “haz un actor, quédate con su dirección y hazte responsable de su muerte”. Esa responsabilidad es lo que separa un juguete de un sistema. Cuando eliges entre spawn y spawnChild, estás decidiendo si tu actor va a mantener una relación con su hijo o solo va a delegarle una tarea; y cuando decides quién llama a stopChild y cuándo, estás dibujando el árbol de supervisión que sostendrá toda la aplicación. El detalle aparentemente menor de dónde guardas la referencia —en el context o en un id— es en realidad la primera decisión de arquitectura del sistema de actores que construirás en las próximas dos lecciones.

⚔️ Engendra y destruye
  1. Crea con spawn, dentro de assign, un actor fromPromise que cargue datos y guarda su referencia en el context.
  2. Lanza con spawnChild un reloj fromCallback que emita TIC cada segundo, identificado por un id.
  3. Añade una transición que detenga el reloj con stopChild y comprueba que su función de limpieza se ejecuta.
  4. Pásale input a un hijo al crearlo y verifica que lo lee para inicializar su propio context.
  5. Completa la tabla de vías de creación con una columna nueva: en qué caso de uso real elegirías cada una.
  6. Explica cuándo elegirías invoke en lugar de spawn, y por qué un actor de larga vida creado y nunca detenido es una fuga de memoria.