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

Comunicación entre actores: el paso de mensajes

Los actores son islas y el mensaje es el único puente. XState v5 ofrece cuatro caminos para tenderlo: sendTo envía un evento a un actor identificado por su referencia o su id; sendParent responde al actor que te invocó; pasar referencias por input hace explícita la relación en vez de asumir un padre implícito; y emit junto con actor.on abre un canal de salida hacia suscriptores externos al sistema. Esta lección explica la semántica asíncrona del paso de mensajes, cuándo usar cada camino y por qué la referencia de un actor es una dirección, no un puntero a su memoria.

⏱ 17 min

Si los actores no comparten memoria, toda coordinación entre ellos tiene que viajar por un solo carril: el mensaje. En el modelo de Hewitt no hay atajos —ni una variable global que ambos leen, ni una llamada directa a un método ajeno—, y esa pobreza aparente es justo lo que mantiene el sistema seguro. XState v5 te da un puñado de formas de enviar mensajes, cada una para una relación distinta entre emisor y receptor. Dominarlas es aprender a dibujar las conversaciones de tu sistema con precisión, sin abrir jamás una grieta en el aislamiento.

🎯 Al terminar esta lección sabrás
  • Enviar eventos a un actor con sendTo, por referencia o por id.
  • Responder al actor invocador con sendParent y entender sus límites.
  • Pasar referencias de actor por input como alternativa explícita al padre implícito.
  • Abrir un canal de salida hacia el exterior con emit y actor.on.

sendTo: enviar a una referencia

sendTo es la herramienta central de la comunicación. Es una acción que deposita un evento en el buzón de otro actor. Le indicas el destino de dos maneras: con la referencia que guardaste al crear el hijo con spawn, o con el id bajo el que lo lanzaste. El evento es un objeto plano y serializable con un campo type, nunca una función ni una referencia a memoria compartida.

import { sendTo } from "xstate"

// Por referencia: usas el ActorRef guardado en el context.
const porReferencia = sendTo(
  ({ context }) => context.hijo,
  { type: "SALUDA" },
)

// Por id: util cuando lanzaste al hijo con spawnChild y un id conocido.
const porId = sendTo("carga1", { type: "REINTENTA" })

// El evento puede construirse a partir del contexto y del evento entrante.
const conDatos = sendTo(
  ({ context }) => context.hijo,
  ({ context }) => ({ type: "CONFIGURA", limite: context.limite }),
)

El envío es asíncrono por definición: sendTo no espera a que el destinatario procese el evento ni recibe un valor de vuelta. Deja el mensaje en el buzón y sigue. Si necesitas una respuesta, el destinatario tendrá que enviarte a ti otro mensaje —el mismo patrón de dirección de respuesta que viste en el pseudocódigo de la primera lección—. Esta asincronía no es un defecto que haya que sortear: es lo que impide que dos actores se bloqueen mutuamente esperándose, el clásico interbloqueo del mundo de los candados.

flowchart LR
A[actor A] -- pregunta con su direccion --> B[actor B]
B -. responde a esa direccion .-> A
style A fill:#89b4fa,color:#11111b
style B fill:#a6e3a1,color:#11111b

sendParent y el patrón de respuesta

Cuando un actor hijo quiere hablarle a quien lo creó, la vía directa es sendParent: envía un evento al buzón del padre sin necesitar su referencia explícita. Es cómodo para el caso clásico en que un hijo termina una tarea y avisa hacia arriba.

import { sendParent } from "xstate"

// Dentro de la maquina hija: avisa al padre de que ha terminado.
const hijo = createMachine({
  initial: "trabajando",
  states: {
    trabajando: {
      on: { LISTO: { actions: sendParent({ type: "HIJO_TERMINO" }) } },
    },
  },
})

sendParent tiene una debilidad: acopla al hijo con la suposición de que siempre tendrá un padre que entiende el evento HIJO_TERMINO. Reutilizar ese hijo bajo otro padre, o como actor raíz, rompe la suposición en silencio. Por eso la comunidad de XState en 2026 prefiere, para lógica reutilizable, la alternativa explícita: pasar la referencia del destinatario por input.

Referencias por input: la relación explícita

En lugar de asumir un padre implícito, el padre puede entregarle al hijo su propia dirección en el momento de crearlo, usando self e input. El hijo recibe esa referencia como un dato de configuración y le responde con sendTo, exactamente igual que a cualquier otro actor. La relación deja de ser un supuesto y pasa a estar escrita en el código.

import { assign } from "xstate"

// El padre pasa SU PROPIA referencia al hijo por input.
const crear = assign({
  hijo: ({ spawn, self }) =>
    spawn("trabajador", { input: { volverA: self } }),
})

// El hijo lee input.volverA y le responde con sendTo, no con sendParent.
const responder = sendTo(
  ({ context }) => context.volverA,
  { type: "HIJO_TERMINO" },
)
💡
Explícito vence a implícito, otra vez

La preferencia por input sobre sendParent es la misma lección que atraviesa todo el track del estado: hacer las dependencias explícitas gana a asumirlas. Un hijo que recibe por input la dirección a la que responder no sabe ni le importa si esa dirección es su padre, un abuelo o un actor hermano; es reutilizable y comprobable en aislamiento. Un hijo que usa sendParent lleva grabada la suposición de un padre concreto, y esa suposición es una dependencia oculta que estalla al recolocarlo. sendParent sigue siendo válido para casos locales y desechables; input es la elección de quien diseña actores para durar.

Puestas todas las piezas juntas, un padre y un hijo se comunican en las dos direcciones sin compartir un solo byte de memoria: el padre crea al hijo pasándole su dirección por input, le envía trabajo con sendTo, y el hijo devuelve el resultado a esa dirección.

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

const hijo = createMachine({
  context: ({ input }) => ({ volverA: input.volverA }),
  on: {
    SUMA: {
      actions: sendTo(
        ({ context }) => context.volverA,
        ({ event }) => ({ type: "RESULTADO", n: event.a + event.b }),
      ),
    },
  },
})

const padre = setup({ actors: { hijo } }).createMachine({
  context: {},
  entry: assign({
    calc: ({ spawn, self }) => spawn("hijo", { input: { volverA: self } }),
  }),
  on: {
    PEDIR: { actions: sendTo(({ context }) => context.calc, { type: "SUMA", a: 2, b: 3 }) },
    RESULTADO: { actions: ({ event }) => console.log("me respondio:", event.n) },
  },
})

emit: hablar hacia fuera del sistema

Las tres vías anteriores comunican actores entre sí, dentro del sistema. Pero a menudo necesitas que el mundo exterior —un componente de UI, un servicio de analítica— reaccione a algo que ocurre dentro de un actor, sin acoplar ese actor a la UI. Para eso está emit: una acción que lanza un evento hacia fuera, que cualquiera suscrito con actor.on puede escuchar. Es la contraparte de salida de subscribe.

import { emit } from "xstate"

// Dentro de la maquina: emite un evento hacia el exterior.
const guardar = emit({ type: "notificacion", texto: "cambios guardados" })

// Fuera, en la UI: te suscribes a ese tipo de evento emitido.
actor.on("notificacion", (evento) => {
  mostrarToast(evento.texto)
})

La diferencia con subscribe es de granularidad y de propósito. subscribe te entrega cada snapshot completa del actor —su estado íntegro tras cada cambio—; actor.on te entrega solo los eventos puntuales que el actor decidió emitir, como avisos discretos. Uno es para reflejar estado; el otro, para señalar acontecimientos. Confundirlos lleva a UIs que se redibujan de más o a notificaciones que se pierden.

Vía Del emisor al receptor Cuándo usarla
sendTo A un actor por referencia o id El caso general entre actores
sendParent De un hijo a su padre implícito Avisos locales y desechables
input con self Referencia entregada al crear Actores reutilizables y comprobables
emit con actor.on De un actor al exterior del sistema Señalar acontecimientos a la UI
La referencia es una direccion, no un puntero

El error mental más costoso al llegar a los actores es confundir una referencia de actor con un puntero a un objeto. Un puntero te da acceso a la memoria: con él lees campos y llamas a métodos. Una referencia de actor te da exactamente una cosa: la capacidad de dejar un mensaje en un buzón. No puedes leer el estado del actor a través de su referencia, no puedes invocar sus métodos, no puedes tocar su context. Solo puedes enviarle un evento y esperar. Esta pobreza deliberada es la que hace honor al primer axioma de Hewitt —influir en otro es únicamente enviarle un mensaje a una dirección que conoces— y es la que sostiene todas las garantías del modelo. Por eso sendTo, sendParent, input y emit no son cuatro APIs arbitrarias, sino cuatro respuestas a una sola pregunta: ¿de dónde saca el emisor la dirección del receptor? De una referencia que guardó al crearlo, del vínculo implícito con su padre, de un dato que le entregaron al nacer, o de una suscripción abierta desde fuera. Cambia la procedencia de la dirección y cambias la topología de las conversaciones de tu sistema; pero el acto en sí —depositar un mensaje asíncrono en un buzón ajeno y no tocar nada más— es siempre el mismo, y es lo único que un actor puede hacerle a otro. Diseñar con actores es, literalmente, decidir quién conoce la dirección de quién.

⚔️ Tiende los puentes
  1. Crea un padre que engendre un hijo con spawn, guarde su referencia y le envíe un evento con sendTo por referencia.
  2. Repite el envío usando el id del hijo en lugar de su referencia y comprueba que llega igual.
  3. Haz que el hijo avise de que terminó, primero con sendParent y luego pasándole la referencia del padre por input y usando sendTo; compara el acoplamiento de cada versión.
  4. Emite un evento con emit desde una transición y escúchalo desde fuera con actor.on; contrasta lo que recibes con lo que te daría subscribe.
  5. Rellena una fila más de la tabla de vías con un quinto caso: dos actores hermanos que necesitan hablarse sin pasar por el padre.
  6. Explica por qué sendTo no puede devolver un valor y cómo se implementa entonces una petición con respuesta entre dos actores.