Acciones de entrada y salida, y guards
Un statechart no solo dice dónde estás y cómo te mueves: también dice qué ocurre al entrar y al salir de cada estado, y bajo qué condiciones se permite una transición. Las acciones de entrada y salida vinculan efectos al ciclo de vida de un estado —arrancar un temporizador al entrar, pararlo al salir— con un orden de ejecución preciso que garantiza limpieza antes que montaje. Los guards son predicados puros que custodian las transiciones: solo se toma la transición si su condición se cumple. Esta lección detalla el orden exacto de acciones en una transición, la diferencia entre transición interna y externa, y por qué guards y acciones deben mantenerse a un lado y otro de una frontera de pureza.
Hasta aquí hemos modelado la topología del comportamiento: qué estados hay, cómo anidan, cómo conviven y cómo se recuerda dónde estabas. Falta lo que hace que un statechart interactúe con el mundo. Dos mecanismos lo aportan. Las acciones de entrada y salida atan efectos secundarios al ciclo de vida de cada estado: algo que sucede exactamente al entrar y algo que sucede exactamente al salir, sin que ninguna transición tenga que acordarse de dispararlo. Los guards ponen condiciones sobre las transiciones: una transición deja de ser incondicional y solo se toma si un predicado se cumple. Juntos completan el formalismo, y su verdadera dificultad no está en la sintaxis, sino en el orden y la pureza: cuándo corre cada efecto y qué tiene prohibido hacer un guard.
- Vincular efectos al ciclo de vida con acciones de entrada y de salida.
- Ordenar con precisión salida, acción de transición y entrada durante un cambio.
- Distinguir la transición interna de la externa por su efecto sobre entrada y salida.
- Escribir guards como predicados puros que custodian las transiciones.
Efectos atados al ciclo de vida
Una acción de entrada se declara con entry y corre cuando el estado se activa; una de salida se declara con exit y corre cuando se abandona. La pareja modela recursos que se adquieren y se liberan. Un estado cargando enciende un spinner al entrar y lo apaga al salir, pase lo que pase después.
import { setup, assign } from 'xstate'
const cargador = setup({
types: {} as { context: { intentos: number; max: number } },
actions: {
mostrarSpinner: () => ui.spinner(true),
ocultarSpinner: () => ui.spinner(false),
contarIntento: assign({ intentos: ({ context }) => context.intentos + 1 }),
},
guards: {
quedanIntentos: ({ context }) => context.intentos < context.max,
},
}).createMachine({
id: 'cargador',
initial: 'inactivo',
context: { intentos: 0, max: 3 },
states: {
inactivo: { on: { PEDIR: 'cargando' } },
cargando: {
entry: ['mostrarSpinner', 'contarIntento'], // efectos al ENTRAR
exit: 'ocultarSpinner', // efecto al SALIR, por cualquier via
on: { OK: 'exito', FALLO: 'evaluando' },
},
evaluando: {
on: {
REINTENTAR: [
{ target: 'cargando', guard: 'quedanIntentos' }, // solo si quedan
{ target: 'error' }, // candidato de reserva sin condicion
],
},
},
exito: { type: 'final' },
error: { type: 'final' },
},
})
La garantía es fuerte: el spinner se apaga en TODA salida de cargando, sea hacia exito o hacia evaluando, sin repetir la orden en cada transición. Igual que un bloque de recursos que se libera sea cual sea la rama por la que sales de una función, la acción de salida centraliza la limpieza en un único lugar ligado al estado, no a los caminos.
stateDiagram-v2 [*] --> Inactivo Inactivo --> Cargando: pedir Cargando --> Exito: ok Cargando --> Evaluando: fallo Evaluando --> Cargando: reintentar si quedan intentos Evaluando --> Error: agotados los intentos Exito --> [*] Error --> [*]
Casi todo efecto con un principio y un final encaja en este molde de entrada y salida. Reconocer el patrón evita el clásico recurso disperso donde el arranque vive en un sitio y la limpieza se olvida en otro.
Temporizadores
Arranca un intervalo o un tiempo de espera al entrar y cancélalo al salir. Sin la acción de salida, el temporizador seguiría vivo tras abandonar el estado y dispararía en el vacío.
Suscripciones
Abre un socket, un canal o un observador al entrar y ciérralo al salir. La pareja entrada y salida es el equivalente exacto de suscribir y dar de baja.
Indicadores visuales
Enciende un spinner, un resaltado o un bloqueo de la UI al entrar y retíralo al salir. El estado gobierna qué se ve, y la UI nunca queda encallada en un indicador huérfano.
Telemetría
Emite un evento de analítica o una traza al entrar en un estado clave. Como corre exactamente una vez por entrada, la métrica cuenta activaciones reales sin duplicados ni olvidos.
El orden exacto durante una transición
Cuando una transición se dispara, los efectos no corren de cualquier manera: siguen un orden que el formalismo fija y que SCXML estandariza. Para una transición del estado de origen al de destino:
- Se evalúa el guard. Si falla, no ocurre nada: ni salida, ni acción, ni entrada.
- Si pasa, corren las acciones de salida de los estados que se abandonan, de dentro hacia afuera, hasta el ancestro común más bajo.
- Corren las acciones propias de la transición.
- Corren las acciones de entrada de los estados en los que se entra, de fuera hacia dentro, hasta la hoja de destino.
El patrón es limpieza antes que montaje: primero se desmonta lo viejo de dentro hacia afuera, luego se monta lo nuevo de fuera hacia dentro. Es exactamente el orden de destructores y constructores en una jerarquía anidada, o el de la limpieza de un efecto antes del siguiente montaje en una UI. Al entrar en un compuesto, además, primero corre la acción de entrada del compuesto y después la de su subestado inicial, respetando el anidamiento de fuera hacia dentro.
Hay una trampa clásica en las transiciones sobre uno mismo. Una transición externa que apunta al propio estado lo abandona y lo reentra: dispara su acción de salida y luego su acción de entrada. Una transición interna, en cambio, se maneja sin salir del estado: no corre ni salida ni entrada, solo sus propias acciones. La distinción importa muchísimo cuando la acción de entrada tiene coste —reinicia un temporizador, relanza una petición— porque tratar una actualización menor como transición externa puede desencadenar un montaje completo indeseado. En XState v5, una transición sin target es interna por defecto y no reejecuta entrada ni salida; en cuanto declaras un target, aunque sea el mismo estado, se vuelve externa y reentra.
Guards: el permiso de la transición
Un guard es un predicado que decide si una transición está permitida. Se evalúa antes de tocar nada, y si devuelve falso, la transición se descarta como si no existiera para ese evento. Cuando un evento ofrece varios candidatos, se prueban en orden y gana el primero cuyo guard pase; un candidato final sin guard actúa como caso por defecto.
// evaluando: primero se prueba el candidato con guard, luego el de reserva
REINTENTAR: [
{ target: 'cargando', guard: 'quedanIntentos' }, // se toma si el predicado pasa
{ target: 'error' }, // si no, cae aqui
],
Esta cadena de candidatos con guards es el equivalente en el statechart a una expresión condicional: una lista de ramas evaluadas en orden hasta que una aplica. La lógica de decisión vive así en la capa de transiciones, declarativa e inspeccionable, en vez de esconderse dentro de un manejador imperativo.
La regla más importante sobre guards es que no pueden tener efectos secundarios. Un guard es una pregunta, no una orden: consulta el contexto y el evento y devuelve un booleano, sin mutar nada, sin lanzar peticiones, sin tocar el mundo. Hay dos razones. La primera es semántica: al evaluar varios candidatos, el intérprete puede probar guards que finalmente no se toman, y un guard con efectos dejaría rastros de transiciones que nunca ocurrieron. La segunda es de testeo: un predicado puro se prueba con una tabla de entradas y salidas, sin montar un entorno. Guarda los efectos para las acciones, que están precisamente para eso, y deja los guards como funciones puras del estado observado. Esa frontera de pureza entre guard y acción es lo que mantiene predecible todo el sistema.
Cuando todos los candidatos de un evento tienen guard y ninguno pasa, y no hay candidato de reserva, la transición no se toma: el evento se descarta y, por el burbujeo, puede subir a un ancestro que sí lo maneje. Esto no es un error, es comportamiento definido: un guard que bloquea deja el sistema donde estaba. Por eso conviene decidir de forma consciente si un evento bloqueado debe ignorarse en silencio o si merece un candidato de reserva que lleve a un estado de aviso; ambas son elecciones de diseño legítimas, pero muy distintas para el usuario.
Con las acciones y los guards, el statechart cierra un círculo que ninguna de sus partes cerraba sola. Un estado responde a dónde estoy; una transición, a qué puede pasar; un guard, a bajo qué condición se permite; una acción, a qué efecto provoca. La potencia no está en tener las cuatro respuestas, sino en tenerlas en un único artefacto declarativo en lugar de repartidas por manejadores imperativos donde cada if, cada efecto y cada cambio de bandera se entrelazan sin forma reconocible. Cuando la condición vive en un guard puro, cuando el efecto vive en una acción atada al ciclo de vida de un estado, y cuando el orden de esos efectos lo fija el formalismo y no la casualidad del código, el comportamiento entero de tu sistema se vuelve un dato: se puede dibujar, simular, verificar y exportar a SCXML sin ejecutar una línea. Ahí está el salto que separa el statechart de una simple organización del código. Un puñado de callbacks también reacciona a eventos, pero no es inspeccionable: para saber qué hace hay que leerlo y ejecutarlo mentalmente. Un statechart es un modelo formal del comportamiento, y las acciones y los guards son lo que lo conecta con el mundo sin romper esa cualidad de modelo. Por eso el consejo final del nivel es una disciplina, no una técnica: empuja toda decisión a un guard, todo efecto a una acción, toda memoria a su clase de estado correspondiente, y deja que la estructura declarativa cargue con la complejidad que de otro modo enterrarías en código imperativo imposible de razonar.
- Implementa el cargador con
entryyexitencargandoy verifica que el spinner se apaga tanto al ir aexitocomo aevaluando. - Traza el orden de acciones de una transición de
cargandoaevaluando: salida primero, luego acción de transición, luego entrada. - Convierte una actualización sobre
cargandoen transición interna y comprueba que no reejecuta entrada ni salida; luego hazla externa y observa la diferencia. - Añade el guard
quedanIntentosy confirma que tras el tercer intento el eventoREINTENTARcae en el candidato de reserva haciaerror. - Intenta meter un efecto dentro de un guard y razona por qué rompe la evaluación de candidatos múltiples.
- Dibuja tu statechart completo en Stately Studio, expórtalo y comprueba que todo el comportamiento quedó capturado como dato, sin código imperativo suelto.