Guards: la transicion que pregunta antes de saltar
No toda transicion debe ocurrir siempre: a veces un estado solo debe cambiar si se cumple una condicion sobre el context o el evento. El guard es un predicado puro y sincrono que XState evalua en el instante de transitar; si devuelve verdadero la transicion procede, si devuelve falso se descarta y se prueba la siguiente candidata. Esta leccion explica el guard como el puente entre la memoria cuantitativa y el control cualitativo, el orden de evaluacion de transiciones candidatas y su fallthrough, los guards parametrizados y los combinadores and, or y not de XState v5, y por que la pureza del guard es innegociable.
Hasta ahora nuestras transiciones eran incondicionales: llega el evento, la maquina salta. Pero los sistemas reales estan llenos de saltos que solo deben producirse bajo cierta condicion —retirar dinero solo si hay fondos, avanzar el asistente solo si el paso es valido, reintentar solo si quedan intentos—. Codificar esa condicion como un estado mas nos devolveria a la explosion combinatoria de la leccion 1. La herramienta correcta es el guard: un predicado que cuelga de la arista de transicion y decide, en el momento exacto del salto, si este ocurre. El guard es donde la memoria cuantitativa que guardamos en el context por fin influye en el control cualitativo, sin dejar de ser memoria. Es, literalmente, la maquina preguntando antes de saltar.
- Definir el
guardcomo un predicado puro que condiciona una transicion leyendo elcontexty elevent. - Entender el orden de evaluacion de transiciones candidatas y el fallthrough hacia la siguiente cuando un
guardfalla. - Escribir guards parametrizados y componer condiciones con los combinadores
and,orynotde XState v5. - Justificar por que un
guarddebe ser puro, sincrono y total, y que se rompe si no lo es.
El guard: un predicado en la arista
Un guard es una funcion que recibe el context y el event y devuelve un booleano. XState la evalua justo antes de tomar la transicion: si devuelve verdadero, el salto procede —con sus actions y su cambio de estado—; si devuelve falso, la transicion se descarta como si no existiera. En XState v5 la propiedad se llama guard —en la v4 se llamaba cond, un cambio que conviene recordar al leer codigo antiguo—.
import { setup } from 'xstate'
const cajero = setup({
types: {
context: {} as { saldo: number; bloqueada: boolean },
events: {} as { type: 'RETIRA'; monto: number },
},
guards: {
hayFondos: ({ context, event }) => context.saldo >= event.monto, // lee context y evento
},
}).createMachine({
context: { saldo: 100, bloqueada: false },
initial: 'activo',
states: {
activo: {
on: {
RETIRA: [
{ target: 'entregando', guard: 'hayFondos' }, // candidata 1
{ target: 'rechazado' }, // candidata 2: fallback
],
},
},
entregando: {},
rechazado: {},
},
})
Aqui esta la resolucion de la tension de la leccion 1: el saldo es un dato cuantitativo y sigue viviendo en el context, no como estados saldo0, saldo1. La distincion cualitativa “hay fondos o no” no se materializa como memoria duplicada, sino que el guard hayFondos la calcula en el instante de transitar. El numero vive una sola vez; la cualidad se deduce cuando importa.
Transiciones candidatas y el fallthrough
Cuando una misma clave de evento tiene varias transiciones —un array— XState las trata como candidatas ordenadas. Las evalua de arriba abajo y toma la PRIMERA cuyo guard pase; una candidata sin guard pasa siempre y actua de fallback. Es la misma semantica que un if / else if / else: solo se ejecuta una rama, y el orden importa.
Coloca las candidatas con las condiciones mas restrictivas arriba y la mas general —o la que no lleva guard— abajo, igual que ordenarias las ramas de un switch. Si pusieras primero una candidata sin guard, capturaria el evento siempre y las de abajo nunca se evaluarian, un error analogo al case inalcanzable. Leer el array de arriba abajo debe contarte la historia completa de la decision.
Hay un detalle facil de olvidar: si NINGUNA candidata pasa su guard y no hay fallback, el evento se ignora en silencio. La maquina no transita, no lanza error, no avisa. Es el comportamiento correcto —un evento no aplicable simplemente no aplica— pero explica muchos “por que no pasa nada al pulsar el boton”: el guard estaba devolviendo falso y no habia salida alternativa.
flowchart TD A[activo] -->|RETIRA y hayFondos| B[entregando] A -->|RETIRA sin fondos| C[rechazado] B --> A C --> A style A fill:#89b4fa,color:#11111b style B fill:#a6e3a1,color:#11111b style C fill:#f38ba8,color:#11111b
Guards parametrizados y combinadores
Un guard escrito como funcion suelta captura su condicion en duro. XState v5 permite parametrizarlos para reutilizar la misma logica con distintos umbrales: el guard recibe un segundo argumento con sus params, y en el punto de uso pasas el objeto de parametros.
guards: {
saldoMinimo: ({ context }, params: { min: number }) => context.saldo >= params.min,
}
// en la transicion, con parametros distintos segun el caso:
guard: { type: 'saldoMinimo', params: { min: 50 } }
Para condiciones compuestas, v5 ofrece los combinadores de orden superior and, or y not, que reciben nombres de guards o guards inline y devuelven un guard nuevo. Evitan enredar la logica booleana dentro de una sola funcion y la dejan legible y componible.
import { and, or, not } from 'xstate'
// retira solo si hay fondos, la cuenta no esta bloqueada, y ademas
// es horario habil O el monto es pequeno
guard: and([
'hayFondos',
not('cuentaBloqueada'),
or(['horarioHabil', { type: 'saldoMinimo', params: { min: 0 } }]),
])
Existe ademas stateIn, un guard que comprueba en que estado esta la maquina —util con estados paralelos, del nivel de statecharts— sin que tengas que espejar esa informacion en el context.
Los guards no solo cuelgan de eventos: una transicion always —sin evento, evaluada de continuo tras cada cambio— usa un guard para saltar en cuanto la condicion se cumple, ideal para estados transitorios que deciden por si solos a donde ir.
// estado transitorio: decide su destino solo con guards, sin esperar evento
verificando: {
always: [
{ target: 'admitido', guard: 'cumpleRequisitos' },
{ target: 'rechazado' }, // fallback si el guard falla
],
}
Recurso suficiente
Retirar dinero, gastar puntos, consumir stock: el guard compara una cantidad del context con la que pide el evento antes de dejar avanzar.
Validez de un paso
En un asistente, SIGUIENTE solo transita si el paso actual es valido; la validez vive en el context y el guard la lee.
Reintentos restantes
Reintentar solo mientras queden intentos: el guard compara el contador con su tope y, al agotarse, cae al estado terminal.
Permisos y roles
Habilitar una accion segun el rol guardado en el context, sin duplicar ese rol como estados distintos ni ramificar el grafo.
La pureza no es opcional
Un guard se evalua para DECIDIR, no para ACTUAR. Debe ser una funcion pura: sin efectos secundarios, sincrona, y determinista —la misma entrada da siempre la misma respuesta—.
Tres prohibiciones. No mutes el context ni el event dentro de un guard: solo debes leerlos. No hagas efectos —nada de fetch, ni logs con consecuencias, ni escrituras—; para eso estan las actions de la leccion 4. Y no dependas de fuentes no deterministas como Date.now o Math.random sin pasarlas por el context o el event: si un guard consulta el reloj directamente, la misma secuencia de eventos puede tomar caminos distintos en dos ejecuciones, y adios a la reproducibilidad. Un guard es una pregunta sobre el estado actual, no una accion sobre el; en cuanto tiene efectos, la transicion deja de ser una funcion pura y el sistema pierde todo lo que lo hacia testeable.
La leccion 1 nos dejo una frontera: los datos cuantitativos viven en el context, los modos cualitativos son estados finitos. El guard es la bisagra que conecta ambos mundos sin romper la separacion. Sin guards, la unica forma de que un numero influyera en el control seria promoverlo a estado —y de ahi la explosion combinatoria que tanto nos costo evitar—. Con guards, el numero permanece donde debe, en la memoria extendida, y solo se consulta en el preciso instante en que una decision cualitativa lo necesita. Piensa en lo que esto significa geometricamente: el grafo de estados sigue siendo pequeno y legible, pero cada arista puede llevar colgada una condicion arbitrariamente rica sobre la memoria infinita. La finitud del control convive con la expresividad del dato. Y como el guard es puro, la transicion completa —evaluar la condicion, decidir el salto, aplicar el assign— sigue siendo una funcion matematica del par estado mas context y del evento: determinista, sin efectos, reproducible. Ese es el equilibrio que ninguna cadena de if dispersos por tu codigo consigue: los guards concentran TODAS las condiciones de transicion en las aristas de un grafo que puedes leer, dibujar y verificar de un vistazo. Dominar los guards es dominar el arte de que la maquina decida mucho sin recordar de mas.
- Modela un asistente de tres pasos donde el evento
SIGUIENTEsolo avance si el paso actual es valido; guarda la validez de cada campo en elcontexty decide con unguard. - Escribe un
RETIRAcon dos candidatas ordenadas —fondos suficientes y fallback arechazado— y comprueba que enviar un monto mayor al saldo cae al fallback. - Envia un evento para el que ninguna candidata pase y sin fallback; verifica que la maquina lo ignora en silencio y explica por que no es un error.
- Convierte
hayFondosen unguardparametrizado con un minimo configurable y usalo con dos umbrales distintos en dos transiciones. - Compon con
andynotuna condicion que exija fondos y cuenta no bloqueada, e intenta —para ver el fallo— colar un efecto dentro de unguard; argumenta por que arruina la reproducibilidad.