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

context: el estado extendido

Una maquina tiene dos memorias que colaboran: los estados finitos, que capturan el modo cualitativo del sistema —un conjunto pequeno y enumerable de situaciones que cambian su comportamiento— y el context, el estado extendido que guarda los datos cuantitativos —contadores, valores de formulario, respuestas del servidor— cuyo dominio es demasiado grande para enumerarlo como estados. Esta leccion define la frontera exacta entre ambos, da un criterio operativo para decidir donde vive cada dato, y muestra por que confundirlos produce la explosion combinatoria de estados que las maquinas nacieron justamente para evitar.

⏱ 16 min

Una maquina de estados finita, tomada al pie de la letra, solo sabe en cual de un punado de modos se encuentra. Pero ningun sistema real vive de modos puros: un reproductor recuerda el segundo exacto, un formulario acumula lo que escribes, una peticion cuenta cuantas veces reintento. Ese dato no cabe en un conjunto finito de estados —hay infinitos segundos posibles— y sin embargo la maquina debe recordarlo. La respuesta de los statecharts es una segunda memoria, ortogonal a la primera: el context, o estado extendido. Entender que toda maquina lleva DOS memorias —una cualitativa y finita, otra cuantitativa y abierta— y saber exactamente que va en cada una es la diferencia entre modelar un sistema y ahogarlo en estados.

🎯 Al terminar esta lección sabrás
  • Distinguir el estado finito, cualitativo y enumerable, del context, cuantitativo y de dominio abierto.
  • Formular el estado total de una maquina como el par formado por el valor finito y el context.
  • Aplicar un criterio operativo para decidir si un dato debe ser un estado o vivir en el context.
  • Reconocer la explosion combinatoria de estados como el sintoma de haber metido datos cuantitativos donde no debian.

Dos memorias: lo cualitativo y lo cuantitativo

Harel, al inventar los statecharts, separo de forma deliberada dos clases de informacion que las maquinas ingenuas mezclaban. Los estados finitos describen el modo del sistema: son cualitativos, mutuamente excluyentes y enumerables —inactivo, cargando, exito, error—. El estado extendido guarda los datos que acompanan a ese modo: son cuantitativos y de dominio abierto —cuantos reintentos llevas, que devolvio el servidor, que texto hay en el campo—. XState llama context a esta segunda memoria.

import { setup, assign } from 'xstate'

const recurso = setup({
  types: {
    context: {} as { intentos: number; datos: string | null; error: string | null },
    events: {} as
      | { type: 'PIDE' }
      | { type: 'RESUELVE'; datos: string }
      | { type: 'RECHAZA'; error: string },
  },
}).createMachine({
  id: 'recurso',
  context: { intentos: 0, datos: null, error: null },   // estado extendido inicial
  initial: 'inactivo',
  states: {
    inactivo: { on: { PIDE: 'cargando' } },
    cargando: { on: { RESUELVE: 'exito', RECHAZA: 'error' } },
    exito: {},
    error: { on: { PIDE: 'cargando' } },
  },
})

El valor finito de esta maquina toma cuatro valores; su context toma infinitos, porque intentos es un entero sin cota y datos es cualquier cadena. Las dos memorias avanzan a la vez pero por reglas distintas: el valor finito solo cambia por una transicion declarada, mientras que el context solo cambia cuando una action lo reescribe.

ℹ️
context es el extended state de Harel

El termino no lo invento XState. En el articulo fundacional de los statecharts de 1987, Harel distingue el control state —el modo finito— del extended state —las variables cuantitativas que el sistema arrastra—. La teoria de automatas llama a esto una maquina de estados finita extendida, o EFSM. XState solo le pone nombre concreto: context. Reconocer que estas ante una idea de cuatro decadas, y no ante un detalle de una libreria de JavaScript, te ayuda a fiarte de la frontera: existe porque separa dos cosas que son distintas en su naturaleza, no por comodidad de implementacion.

El estado total es un par

Aqui esta el punto que casi todos pasan por alto: el estado real de la maquina no es su valor finito, sino el par formado por el valor finito y el context. Dos actores en el modo cargando pero con intentos distintos NO estan en el mismo estado total; comparten modo pero difieren en memoria extendida. El snapshot que devuelve XState lo refleja: snapshot.value es el modo cualitativo y snapshot.context es la memoria cuantitativa, y solo juntos determinan que hara la maquina ante el proximo evento.

import { createActor } from 'xstate'

const actor = createActor(recurso).start()
actor.send({ type: 'PIDE' })
actor.send({ type: 'RECHAZA', error: 'timeout' })

const s = actor.getSnapshot()
s.value           // 'error'  -> el modo cualitativo
s.context.error   // 'timeout' -> el dato cuantitativo que lo acompana
s.matches('error') // true

Esta dualidad explica por que una maquina con pocos estados puede modelar un sistema riquisimo: los cuatro modos capturan las situaciones que cambian el comportamiento, y el context carga con todo el detalle que no lo cambia. La finitud vive en el control; la infinitud, en la memoria.

El criterio: estado finito o context

La pregunta practica en cada dato es: donde lo pongo. El criterio decisivo no es el tipo del dato sino su papel en el comportamiento.

💡
La prueba de la enumerabilidad y del comportamiento

Un dato es un estado finito si sus valores posibles son pocos, mutuamente excluyentes, y cada uno habilita transiciones o comportamientos distintos: abierto frente a cerrado, autenticado frente a anonimo. Un dato va en el context si su dominio es grande o infinito, o si su valor concreto no cambia por si mismo que transiciones estan disponibles: un saldo, un texto, un contador, una lista. Regla de bolsillo: si puedes dibujar los valores como cajas en un diagrama y trazar flechas entre ellas, es estado; si tendrias que dibujar una caja por cada numero, es context.

🚦

Modo de conexion: estado

conectado, desconectado y reconectando son pocos, excluyentes y cambian que puedes hacer. Enumerables y con comportamiento propio: son estados finitos.

🔢

Contador de reintentos: context

El numero de intentos es un entero sin cota util. No lo enumeras como estados: vive en el context y lo consulta un guard.

✍️

Texto de un campo: context

El contenido de un input tiene dominio infinito. Jamas es un estado; es memoria cuantitativa pura que se reescribe con assign.

🔐

Sesion: estado, no flag suelto

anonimo frente a autenticado habilita rutas y acciones distintas. Aunque parezca un flag, es un modo cualitativo: modelalo como estado.

Un caso limite aclara la frontera. El saldo de una cuenta es cuantitativo y vive en el context. Pero la distincion cualitativa “hay saldo o no lo hay” SI es un modo, si de ella depende que transiciones existan. La solucion elegante no es duplicar el dato en dos sitios: es dejar el numero en el context y expresar la condicion cualitativa con un guard que lo lea en el momento de transitar —el tema de la leccion 3—. El dato vive una sola vez; la maquina lo consulta cuando la cualidad importa.

La explosion combinatoria y su cura

Que pasa si ignoras la frontera y modelas datos cuantitativos como estados. Imagina un contador de 0 a 100 codificado como cien estados cuenta0, cuenta1, hasta cuenta100, cada uno con sus transiciones. Ahora cruzalo con tres modos de un formulario y con un flag de carga: los estados se multiplican. Ese es el pecado que las maquinas nacieron para evitar y en el que caen quienes olvidan el context.

// ANTIPATRON: codificar una cantidad como estados finitos
states: {
  intento0: { on: { FALLA: 'intento1' } },
  intento1: { on: { FALLA: 'intento2' } },
  intento2: { on: { FALLA: 'agotado' } },   // ...y asi sin fin
}
// CURA: un solo estado y la cantidad en el context
states: {
  cargando: {
    on: {
      FALLA: {
        target: 'cargando',   // se queda; solo cambia la memoria
        actions: assign({ intentos: ({ context }) => context.intentos + 1 }),
      },
    },
  },
}

Los mismos reintentos, dos modelos opuestos: arriba, un estado nuevo por cada numero, un grafo que crece sin fin y transiciones duplicadas; abajo, un unico estado que incrementa un entero. La cantidad pertenece a la memoria, no al control, y en cuanto la devuelves a su sitio el diagrama recupera su tamano.

flowchart LR
I[inactivo] -->|PIDE| C[cargando]
C -->|RESUELVE| E[exito]
C -->|RECHAZA| F[error]
F -->|PIDE| C
style I fill:#89b4fa,color:#11111b
style E fill:#a6e3a1,color:#11111b
style F fill:#f38ba8,color:#11111b

En el diagrama solo hay cuatro cajas: los modos que de verdad cambian el comportamiento. Los intentos, el error textual, los datos devueltos —todo lo cuantitativo— no aparece como caja porque viaja en el context, invisible en el grafo pero presente en cada snapshot. Anadir un quinto reintento no crea un estado nuevo: solo incrementa un entero.

⚠️
El sintoma: estados que solo se diferencian en un numero

Si al modelar te descubres creando estados casi identicos que solo difieren en una cantidad —unIntento, dosIntentos, tresIntentos—, has puesto en el control lo que pertenece a la memoria. La cura es siempre la misma: colapsa esos estados en uno solo y mueve la cantidad al context. La maquina se encoge, el diagrama vuelve a ser legible, y la logica que dependia del numero pasa a un guard. Una maquina sana tiene tantos estados como modos cualitativos tenga el problema, ni uno mas.

La finitud no esta en el sistema, esta en donde eliges mirarlo

La leccion profunda es que “maquina de estados FINITA” nunca significo que el sistema tenga finita informacion —tiene infinita, arrastra numeros sin cota— sino que su CONTROL es finito. Separar context de estados finitos es un acto de compresion deliberada: decides que un conjunto pequeno de modos cualitativos gobierne el comportamiento, y relegas todo lo cuantitativo a una memoria que acompana pero no ramifica. Esa decision es lo que hace tratable el sistema, porque puedes razonar exhaustivamente sobre un grafo de cuatro cajas de un vistazo, cosa imposible sobre un espacio de estados infinito. Cuando un dato exige mirarlo como modo, promuevelo a estado; cuando solo exige recordarlo, dejalo en el context y consultalo con un guard. Dominar esta frontera no es una convencion de XState: es la tecnica central de toda la ingenieria de estados, la que convierte la complejidad accidental de mil combinaciones en la complejidad esencial de unos pocos modos y una memoria que los acompana. El resto de este nivel —assign, guard, entry, exit— son las herramientas para operar esa memoria sin romper jamas la finitud del control.

⚔️ Traza la frontera
  1. Toma un reproductor de video y lista sus datos: modo de reproduccion, segundo actual, volumen, si esta a pantalla completa. Clasifica cada uno como estado finito o context justificando con la prueba de la enumerabilidad.
  2. Escribe la maquina con setup y createMachine: pon los modos como estados y todo lo cuantitativo en el context tipado.
  3. Envia dos secuencias de eventos que dejen la maquina en el mismo value pero distinto context, e imprime ambos snapshots para verlo.
  4. Encuentra en tu codigo, o inventa, un caso de explosion: estados que solo difieren en una cantidad. Colapsalos en uno y mueve el numero al context.
  5. Argumenta en dos frases por que el estado total es el par valor mas context y no solo el value.