wandres.dev
XSTATE: CONTEXT Y LÓGICA · guards y actions

Actions: entry, exit y la frontera del efecto

Las actions son lo que una maquina HACE cuando algo ocurre, y se organizan en dos ejes ortogonales. El primer eje es el momento: entry al entrar en un estado, exit al salir de el, y las actions de la propia transicion sobre la arista. El segundo eje, mas profundo, es la naturaleza: una action pura como assign o raise solo altera de forma determinista el estado interno de la maquina y queda capturada en el snapshot, mientras que un efecto es una llamada fire and forget al mundo exterior —un log, una peticion, enfocar un input— que el interprete dispara pero que no forma parte del estado. Esta leccion diseca ambos ejes, fija el orden exacto exit, transicion y entry, y explica por que separar la action pura del efecto es lo que hace testeable a toda la maquina.

⏱ 17 min

Una maquina que solo cambia de estado seria muda: sabria en que modo esta pero no haria nada al respecto. Las actions son su forma de actuar —contar una apertura, avisar al usuario, arrancar un temporizador, enfocar un campo—. XState las organiza en dos ejes que conviene no confundir. El primero es CUANDO se disparan: al entrar en un estado con entry, al salir con exit, o al recorrer una arista con las actions de transicion. El segundo, mas sutil y mas importante, es QUE CLASE de action es: unas son puras y solo tocan la memoria interna de la maquina —como el assign que ya conoces—; otras son efectos que salen al mundo y no vuelven. Trazar esa segunda frontera con nitidez es lo que separa una maquina que puedes testear en aislamiento de una maraña de llamadas imposible de reproducir.

🎯 Al terminar esta lección sabrás
  • Distinguir los tres momentos de una action: entry al entrar, exit al salir, y la action sobre la propia transicion.
  • Fijar el orden exacto de ejecucion en una transicion entre estados: exit del origen, actions de la arista y entry del destino.
  • Separar la action PURA —assign, raise— que altera el estado interno, del EFECTO que impacta el mundo exterior.
  • Usar entry y exit para montar y desmontar efectos de forma simetrica sin fugas.

Tres momentos: entry, exit y transicion

Una action puede colgar de tres sitios. En entry, se ejecuta cada vez que la maquina ENTRA en ese estado, venga de donde venga. En exit, cada vez que lo ABANDONA. Y en la clave de un evento, se ejecuta al TOMAR esa transicion concreta. Los tres aceptan una action o una lista de ellas.

import { setup, assign } from 'xstate'

const puerta = setup({
  types: {
    context: {} as { aperturas: number },
    events: {} as { type: 'ABRE' } | { type: 'CIERRA' },
  },
  actions: {
    contarApertura: assign({ aperturas: ({ context }) => context.aperturas + 1 }), // pura
    avisar: (_, params: { texto: string }) => console.log(params.texto),           // efecto
  },
}).createMachine({
  context: { aperturas: 0 },
  initial: 'cerrada',
  states: {
    cerrada: {
      on: { ABRE: { target: 'abierta', actions: 'contarApertura' } },  // action de transicion
    },
    abierta: {
      entry: { type: 'avisar', params: { texto: 'puerta abierta' } },  // al entrar
      exit: { type: 'avisar', params: { texto: 'puerta cerrada' } },   // al salir
      on: { CIERRA: 'cerrada' },
    },
  },
})

La eleccion del momento no es estetica, es semantica. Una action en entry se ejecuta sin importar por que arista llegaste al estado —ideal para lo que SIEMPRE debe pasar al estar en ese modo—. Una action de transicion solo corre por ESA arista —ideal para lo que depende del camino concreto—. Si algo debe ocurrir al entrar a cargando da igual desde donde, es entry; si algo depende de que vengas de error y no de inactivo, es action de transicion.

Un matiz util: una transicion hacia el MISMO estado vuelve a disparar exit y entry si es externa, mientras que una transicion interna no lo hace. Esto te da control fino: si quieres que reentrar en un modo reinicie sus efectos —recontar, reenfocar—, usa una transicion externa a si mismo; si solo quieres tocar el context sin remontar nada, usa una interna. La misma flecha, dos semanticas, segun cruce o no la frontera del estado.

💡
entry y exit son el montaje y desmontaje del estado

Piensa en entry y exit como el constructor y el destructor de un estado. Todo lo que un modo necesita montar al empezar —arrancar un intervalo, suscribirse, enfocar un input— va en entry; todo lo que debe desmontar al terminar —limpiar ese intervalo, desuscribirse— va en exit. Mantener la simetria evita la clase de fuga mas comun en interfaces reactivas: el efecto que se monta y nunca se limpia. Si tu entry abre algo, tu exit deberia cerrarlo.

Conviene fijar el habito desde el principio: para cada efecto, preguntate no solo QUE hace, sino en que MOMENTO del ciclo de vida del estado debe ocurrir. La mayoria de los bugs de interfaces reactivas nacen de poner en entry algo que dependia del camino y debia ir en la arista, o de olvidar en exit la limpieza de lo que entry habia montado. Pensar en terminos de momento, y no solo de accion, desactiva ambas trampas de raiz.

El orden dentro de una transicion

Cuando un evento provoca un salto de un estado a otro, XState ejecuta las actions en un orden fijo y garantizado. Primero evalua el guard de la leccion 3; si pasa, ejecuta las exit del estado que se abandona, luego las actions de la transicion, y por ultimo las entry del estado al que se llega. Dentro de cada grupo, el orden es el que declaraste —recuerda la linealidad de v5 de la leccion 2—.

sequenceDiagram
participant G as guard
participant S as estado origen
participant T as arista
participant D as estado destino
G->>G: evalua la condicion primero
S->>S: ejecuta exit del origen
T->>T: ejecuta actions de la transicion
D->>D: ejecuta entry del destino

Este orden importa cuando las actions comparten context. Si el exit del origen limpia un dato y el entry del destino lo lee, veras el dato ya limpio, porque exit corre antes. Conocer la secuencia exit, transicion, entry elimina una fuente entera de bugs de “por que este valor ya estaba cambiado cuando lo lei”.

// el orden exit -> transicion -> entry es observable en la consola
states: {
  a: {
    exit: () => console.log('1 exit de a'),
    on: { IR: { target: 'b', actions: () => console.log('2 action de transicion') } },
  },
  b: { entry: () => console.log('3 entry de b') },
}
// enviar IR estando en a imprime, en este orden exacto: 1, 2, 3

Que este orden sea una garantia del modelo, y no un accidente de implementacion, es lo que te deja predecir el comportamiento sin ejecutarlo: dado un estado, un evento y las actions declaradas, la secuencia es siempre la misma. La reactividad ingenua de los signals no ofrecia esta certeza sobre CUANDO corre cada efecto; una maquina si, porque su ciclo de vida esta escrito en la propia definicion.

Action pura frente a efecto

Aqui esta el eje profundo. Una action pura solo modifica el estado interno de la maquina de forma determinista y queda registrada en el snapshot: assign reescribe el context, raise inyecta un evento interno que se procesara a continuacion. Un efecto es cualquier funcion que impacta el mundo exterior —imprimir, pedir por red, tocar el DOM, disparar analitica— y que la maquina lanza sin esperar respuesta: fire and forget.

actions: [
  assign({ aperturas: ({ context }) => context.aperturas + 1 }), // PURA: cambia el context
  ({ context }) => trackAnalytics(context.aperturas),            // EFECTO: sale al mundo
]

La diferencia no es de sintaxis sino de consecuencias. El interprete de XState calcula PRIMERO el proximo snapshot aplicando las actions puras —el nuevo value y el nuevo context— y solo DESPUES dispara los efectos. Por eso el snapshot resultante es funcion pura del estado, el context y el evento: los efectos no lo alteran, solo salpican hacia fuera. Esta separacion es la que permite, como veremos en la leccion 5, calcular una transicion sin ejecutar sus efectos.

📥

assign es pura

Reescribe el context de forma inmutable. Su resultado queda en el snapshot: es parte de la funcion de transicion.

🔁

raise es pura

Encola un evento interno para la propia maquina. No sale al mundo; alimenta el bucle de la maquina consigo misma.

📡

emit y sendTo son efecto

Publican eventos hacia fuera: emit a los suscriptores, sendTo a otro actor. Cruzan la frontera de la maquina.

🌍

funcion inline es efecto

Imprimir, medir, tocar el DOM. Fire and forget: impacta el mundo y nunca regresa al snapshot.

La consecuencia practica es una regla de oro para depurar y testear: si algo debe quedar registrado en el estado y sobrevivir al evento, tiene que ser una action pura; si solo debe ocurrir de cara al exterior y no cambia lo que la maquina ES, es un efecto. Cuando dudes, preguntate si su resultado deberia aparecer en el snapshot: si la respuesta es si, es pura; si da igual porque ya salio al mundo, es un efecto.

💡
enqueueActions para decidir en tiempo de ejecucion

Cuando que actions disparar depende del context o del evento, enqueueActions te da una funcion con un enqueue que llamas de forma condicional: si el usuario es premium, enqueue('daBonus'); si no, nada. Es la manera limpia de expresar logica de la forma si esto entonces esta action sin ramificar el grafo ni duplicar transiciones, y sigue siendo declarativa: describes que se encolara y el interprete lo ejecuta en el orden en que lo encolaste.

📝
El vocabulario de acciones de XState v5

Mas alla de assign, v5 trae un repertorio de action creators con semantica precisa. raise encola un evento interno para la propia maquina —comunicacion consigo misma—. sendTo envia un evento a otro actor. emit publica un evento hacia los suscriptores externos —un efecto de salida—. log es un efecto de registro integrado. Y enqueueActions te deja encolar actions de forma condicional dentro de una funcion, leyendo el context para decidir cuales disparar. Saber cual es interno —raise— y cual sale al mundo —emit, sendTo, log— es saber cuales son puros de cara al snapshot y cuales son efectos.

⚠️
Nada de asincronia en una action

Una action, pura o efecto, se dispara y termina; no espera. No pongas await, ni promesas que la maquina deba seguir, ni logica de reintento dentro de una action. Un efecto legitimo es “lanza esta peticion y olvidala”; si necesitas su resultado para transitar, eso NO es una action sino un actor invocado con invoke —el ciclo de vida, el resultado y el error de lo asincrono se modelan como un actor hijo, tema del siguiente nivel—. La regla operativa: si te importa CUANDO termina o QUE devuelve, no es una action; es un actor.

Dos ejes ortogonales: cuando ocurre y de que naturaleza es

El error de principiante es pensar que entry, exit y transicion son “tipos de action”. No lo son: son MOMENTOS. Cualquier action —pura o efecto— puede colgar de cualquiera de los tres. Los dos ejes son independientes y hay que llevarlos en la cabeza a la vez. El eje del momento —entry, exit, transicion— responde a CUANDO se dispara la action y lo decides por la semantica del flujo: lo que siempre pasa al estar en un modo va en entry, lo que limpia va en exit, lo que depende del camino va en la arista. El eje de la naturaleza —pura frente a efecto— responde a QUE le hace la action al sistema y determina la testeabilidad: las puras entran en el snapshot y son parte de la funcion de transicion; los efectos salen al mundo y quedan fuera de ella. Cuando dominas los dos ejes juntos, escribes maquinas donde cada cosa esta en su sitio por una razon: un assign en entry inicializa la memoria del modo; un efecto en exit cierra un recurso; un raise en una transicion encadena un paso interno. Y sobre todo, mantienes limpia la linea que separa lo que la maquina ES —su snapshot, puro y reproducible— de lo que la maquina PROVOCA —sus efectos, sucios e irreversibles—. Esa linea es la que la leccion 5 convertira en tu mejor herramienta de test: si los efectos estan aislados y nombrados, puedes ejercitar toda la logica de la maquina sin que ni una sola peticion salga de verdad.

⚔️ Coloca cada accion en su sitio
  1. Modela un modal con estados cerrado y abierto; en el entry de abierto enfoca el primer input y en el exit devuelve el foco al disparador. Verifica la simetria montaje y desmontaje.
  2. Anade una action de transicion que solo corra al pasar de error a cargando —un log de reintento— y comprueba que NO corre al entrar a cargando desde inactivo.
  3. Marca en una transicion con tres actions cuales son puras y cuales efectos; predice el orden en que se ejecutan y confirmalo con la salida.
  4. Reescribe un efecto que hoy hace await fetch para que sea, en su lugar, un actor invocado; explica por que no puede seguir siendo una action.
  5. Argumenta en dos frases por que el interprete aplica las actions puras antes de disparar los efectos, y que se rompe si lo hiciera al reves.