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

El sistema de actores: la jerarquía y el system

Los actores individuales no valen por sí solos: valen porque se componen en un sistema. Al instanciar el actor raíz con createActor nace un system —un árbol de supervisión con una raíz, una jerarquía padre-hijo y un registro compartido—. Con systemId un actor se hace descubrible por nombre en todo el árbol, y desde cualquier actor se accede a él con self.system y system.get. Esta lección cierra el nivel 13 explicando por qué esta estructura —aislamiento, composición y localidad del fallo— es la que permite que el modelo escale de un contador a una aplicación entera sin que la complejidad se vuelva ingobernable.

⏱ 18 min

Un actor aislado es poco más que una curiosidad. La potencia del modelo aparece cuando muchos actores se organizan en una estructura viva: un árbol donde cada padre supervisa a sus hijos, con una raíz que lo sostiene todo y un registro común que permite encontrarse sin conocerse de antemano. XState llama a esa estructura el sistema. Es la culminación de los tres axiomas de Hewitt y la respuesta a la pregunta que abría el nivel: ¿por qué este modelo de 1973 escala a lógica arbitrariamente compleja cuando otros se ahogan? Porque el sistema de actores no es una colección de piezas, sino un árbol de responsabilidades.

🎯 Al terminar esta lección sabrás
  • Ver la jerarquía padre-hijo como un árbol de supervisión con una raíz.
  • Registrar actores con systemId para hacerlos descubribles en todo el sistema.
  • Acceder al registro compartido desde cualquier actor con self y system.get.
  • Explicar por qué el aislamiento, la composición y la localidad del fallo hacen que el modelo escale.

El árbol de actores: raíz, padres, hijos

Cuando instancias un actor con createActor y lo arrancas, no creas solo un actor: creas un sistema. Ese primer actor es la raíz, y todo lo que engendre —con spawn, spawnChild o invoke— cuelga de él formando un árbol. Cada actor tiene exactamente un padre (salvo la raíz, que no tiene ninguno) y puede tener cualquier número de hijos. La estructura es idéntica a la de un árbol de supervisión de Erlang, y no por casualidad.

flowchart TD
R[actor raiz la app] --> A[actor auth]
R --> C[actor carrito]
C --> C1[actor item 1]
C --> C2[actor item 2]
R -. descubre por systemId .-> A
style R fill:#cba6f7,color:#11111b
style A fill:#89b4fa,color:#11111b
style C fill:#89b4fa,color:#11111b
style C1 fill:#a6e3a1,color:#11111b
style C2 fill:#a6e3a1,color:#11111b

La jerarquía no es decorativa: gobierna el ciclo de vida. Cuando un actor se detiene, se lleva consigo a todos sus descendientes; detener la raíz apaga el sistema entero de forma ordenada, de las hojas hacia el tronco, ejecutando cada limpieza a su paso. Esta contención del ciclo de vida por subárboles es lo que evita que detener una parte de la app deje procesos huérfanos vivos en otra. El árbol no es un diagrama que dibujas después para documentar; es la estructura que XState usa en tiempo de ejecución para decidir quién vive y quién muere, y cuándo.

El system: registro y descubrimiento

El árbol resuelve la relación entre padres e hijos, pero muchos actores necesitan hablar con otros que no son ni sus padres ni sus hijos: el carrito quiere consultar al actor de autenticación, que vive en otra rama. Pasar referencias a mano por todo el árbol —el temido prop drilling del nivel de contexto— sería insufrible. Para eso existe el system: un registro compartido por todos los actores del árbol, donde un actor puede publicarse con un nombre global y otro puede encontrarlo por ese nombre.

import { createActor } from "xstate"

// La raiz define el sistema. Se le puede dar un systemId.
const app = createActor(maquinaApp, { systemId: "app" })
app.start()
import { assign, sendTo } from "xstate"

// Un hijo se registra con systemId al ser creado: ahora es descubrible.
const registrar = assign({
  auth: ({ spawn }) => spawn("autenticacion", { systemId: "auth" }),
})

// Cualquier actor del sistema lo encuentra por su nombre, sin tener su referencia.
const cerrarSesion = sendTo(
  ({ system }) => system.get("auth"),
  { type: "LOGOUT" },
)

La diferencia entre el id de la lección 3 y el systemId de esta es la diferencia entre lo local y lo global, y merece un cuadro claro porque confundirlos produce actores que no se encuentran o nombres que chocan.

Identificador Ámbito Para qué sirve
id Local a un padre El padre distingue a sus propios hijos
systemId Global al sistema Cualquier actor descubre a otro por nombre

Dos actores distintos pueden tener hijos con el mismo id sin colisionar, porque el id solo tiene sentido dentro de un padre. El systemId, en cambio, es único en todo el sistema y sirve para el descubrimiento a distancia: es la guía telefónica del árbol, y por eso un systemId repetido es un error.

ℹ️
self y system: las coordenadas de cada actor

Dentro de cualquier acción o assign, el objeto que recibes trae self —la referencia al propio actor, útil para pasarla por input o para suscribirse a sí mismo— y system —la puerta al registro compartido—. Con self un actor conoce su propia dirección; con system.get alcanza la de cualquier otro que se haya publicado con un systemId. Juntos dan a cada actor sus coordenadas: sabe quién es y puede localizar a los demás sin que nadie tenga que enhebrarle referencias a través de media docena de niveles.

Este acceso al registro también permite que un actor localice a sus vecinos al nacer, en lugar de esperar a que le manden sus direcciones. Un carrito puede pedirle al system la dirección de auth en el inicializador de su propio context:

import { setup, sendTo } from "xstate"

const carrito = setup({}).createMachine({
  // Al nacer, localiza a auth por su systemId y guarda su direccion.
  context: ({ system }) => ({ auth: system.get("auth") }),
  on: {
    PAGAR: { actions: sendTo(({ context }) => context.auth, { type: "VERIFICA" }) },
  },
})

Y la raíz teje el árbol entero: engendra a los grandes subsistemas, registra con systemId a los que deben ser descubribles desde cualquier rama, y a partir de ahí cada subárbol se organiza por su cuenta.

import { setup, createActor, spawnChild } from "xstate"

const app = setup({
  actors: { auth, carrito },
}).createMachine({
  entry: [
    spawnChild("auth", { systemId: "auth" }),        // descubrible por todos
    spawnChild("carrito", { id: "carrito" }),
  ],
})

createActor(app, { systemId: "app" }).start()         // aqui nace el sistema

Por qué el modelo escala

Aquí converge todo el nivel. Tres propiedades, que en las lecciones anteriores aparecían sueltas, se revelan ahora como las tres razones por las que un sistema de actores crece sin colapsar.

🧊

Aislamiento

Ningún actor comparte memoria con otro. Añadir el actor número mil no introduce una sola condición de carrera nueva, porque no hay estado que dos actores puedan pisar a la vez.

🧩

Composición

Un actor puede contener actores, que a su vez contienen actores. Un statechart invoca una promesa que habla con un callback, y en cada frontera el contrato es el mismo. La complejidad se anida, no se enreda.

🛡️

Localidad del fallo

Un error queda encerrado en el actor que lo sufre y en su subárbol. El padre lo recibe como un mensaje y decide qué hacer; los actores hermanos, en otras ramas, ni se enteran.

La localidad del fallo merece detenerse. En un sistema de estado compartido, un error a mitad de una mutación puede dejar los datos en un estado imposible que envenena todo lo que los lea después. En un árbol de actores, el radio de daño de un fallo está acotado por la estructura: como un actor solo puede afectar a otros enviándoles mensajes, y como el estado corrupto vive encerrado tras su buzón, el desastre no se filtra. El padre, que supervisa, decide si reinicia al hijo caído, lo ignora o escala el problema. Es la traducción a estado de aplicación de la filosofía let it crash de Erlang: no intentes que nada falle jamás; haz que el fallo de una parte no arrastre al todo.

La composición es la segunda palanca de escala, y es la que distingue al modelo de una simple colección de máquinas. Como un actor puede ser hijo de otro y padre de terceros, un subsistema entero —el carrito con sus ítems, cada uno con su propia lógica— se comporta hacia fuera como un único actor con un buzón y una snapshot. Puedes razonar sobre el carrito sin abrir sus ítems, igual que razonas sobre una función sin leer su cuerpo. Esa capacidad de encapsular un subárbol tras una sola dirección es lo que mantiene manejable un sistema de miles de actores.

💡
El systemId es poderoso: no lo conviertas en variable global

system.get resuelve el descubrimiento sin prop drilling, pero abusar de él recrea el problema del que huíamos: un registro global del que todos tiran vuelve opacas las dependencias. Registra con systemId solo los actores verdaderamente transversales —autenticación, enrutado, telemetría— y para el resto pasa referencias por input. El registro es una guía telefónica, no un cajón de sastre donde todo el mundo mete la mano.

El sistema de actores es el estado como sociedad, no como almacen

Al empezar el track pensabas el estado como un almacén: un lugar donde guardar datos y del que leerlos, y toda la disciplina consistía en no desincronizar las copias. El nivel 13 propone una imagen radicalmente distinta y más poderosa: el estado como una sociedad de entidades vivas. En esta imagen no hay un almacén central que todos consultan, sino un árbol de actores, cada uno dueño absoluto de su pedazo de estado, que colaboran enviándose mensajes como personas que se pasan notas en lugar de leerse la mente. Y esta sociedad escala por las mismas razones por las que escala una organización humana bien diseñada: porque cada miembro es autónomo y responsable de lo suyo (aislamiento), porque los equipos se agrupan en equipos mayores sin cambiar cómo colaboran (composición), y porque el error de una persona no paraliza a toda la empresa, sino que lo absorbe su responsable directo (localidad del fallo). El system es el organigrama de esa sociedad: define quién supervisa a quién y da a cada miembro una guía para encontrar a los demás. Cuando interiorizas esto, la pregunta del diseño de estado cambia de raíz. Ya no preguntas “¿dónde guardo este dato y cómo evito que se desincronice?”, sino “¿qué actor es el dueño de este dato, quién lo supervisa, y a quién necesita enviar mensajes?”. Esa segunda pregunta es la que escala a un millón de líneas, y es la herencia intacta de una idea que Carl Hewitt tuvo en 1973 y que XState v5, en 2026, pone en tus manos con cuatro funciones.

📝
Hacia el nivel 14

Has visto crear actores imperativamente con spawn y spawnChild, comunicarlos con sendTo y emit, y organizarlos en un sistema con systemId. Falta la vía declarativa de invocar actores atados a un estado —invoke— y el manejo fino de promesas, observables y callbacks como servicios con su resultado y su error. Ese es el nivel 14, que apoyará cada una de sus piezas sobre el modelo de actores que acabas de construir aquí.

⚔️ Construye el sistema
  1. Instancia una máquina raíz con createActor y un systemId; comprueba que arrancarla crea el sistema.
  2. Engendra dos hijos en ramas distintas y registra uno de ellos con un systemId propio.
  3. Desde el otro hijo, localiza al primero con system.get y envíale un evento con sendTo, sin haber pasado nunca su referencia a mano.
  4. Detén la raíz y observa el orden en que se detienen los descendientes y se ejecutan sus limpiezas.
  5. Contrasta en la tabla el id local y el systemId global, y explica por qué dos padres pueden reusar el mismo id sin colisión.
  6. Argumenta, con las tres propiedades del sistema, por qué añadir el actor número mil no vuelve el sistema más frágil, y relaciónalo con el let it crash de Erlang.