wandres.dev
QUÉ ES EL ESTADO · la raíz de la complejidad

Qué es el estado

El estado son los datos que un programa recuerda entre un evento y el siguiente: la memoria que sobrevive a la llamada y condiciona la próxima respuesta. Aquí se define con precisión, se formaliza un programa con estado como un transductor y no como una función pura, y se explica por qué esa memoria persistente rompe la transparencia referencial y convierte el razonar sobre el código en razonar sobre toda su historia.

⏱ 14 min

Antes de elegir un signal, un store o una máquina de estado hay que responder una pregunta más básica y más honda: qué es exactamente el estado. La respuesta corta es engañosamente simple —son los datos que cambian en el tiempo—, pero dentro de ella se esconde la razón por la que el software es difícil. El estado es la memoria que un programa arrastra de un instante al siguiente, y esa memoria es justo lo que hace que razonar sobre el código deje de ser aritmética y se vuelva historia.

🎯 Al terminar esta lección sabrás
  • Definir el estado con precisión: la información que persiste entre eventos y condiciona la próxima respuesta.
  • Formalizar un programa con estado como un transductor, no como una función pura.
  • Entender por qué el estado rompe la transparencia referencial y multiplica los caminos de ejecución.
  • Reconocer el estado como la entrada oculta que vuelve impredecible a cualquier programa.

Una definición precisa: memoria que sobrevive al evento

Un programa sin estado es una función: recibe una entrada, calcula una salida y lo olvida todo. mayusculas("hola") devuelve HOLA y no deja rastro; invocarla mil veces no cambia nada en el mundo. El estado nace en el instante en que el programa necesita recordar algo de una llamada para la siguiente: el saldo de una cuenta, la sesión de un usuario, los ítems de un carrito, la posición del scroll. El estado es la información que persiste entre dos eventos y condiciona cómo se responde al segundo.

La palabra clave es persiste. Una variable local que nace y muere dentro de una función no es estado en el sentido que nos importa: es un cálculo intermedio, un andamio que se desmonta al terminar. El estado es lo que sobrevive al evento que lo creó y espera, latente, a influir en el próximo. Por eso hablamos de datos “que cambian en el tiempo”: no porque el reloj importe, sino porque hay un antes y un después separados por eventos, y el dato es el puente que los une.

La idea tiene un linaje antiguo. En la teoría de autómatas, un autómata finito no es más que un conjunto de estados y transiciones sin memoria adicional; una máquina de Turing añade una cinta —memoria mutable e ilimitada— y con ella el poder de computar cualquier cosa. Ese salto del autómata a la máquina con cinta es exactamente el salto de un programa sin estado a uno con estado: la cinta es el primer “estado” de la informática. La arquitectura de von Neumann lo grabó en el hardware con memoria mutable compartida entre instrucciones y datos. El estado no es un vicio de nuestros lenguajes; está en los cimientos del modelo de cómputo que usamos.

Formalmente, un programa con estado no responde al modelo salida = f(entrada). Responde a algo parecido a una máquina de Mealy: una función de transición que toma la entrada y el estado actual y produce la salida y el estado siguiente, (entrada, estado) -> (salida, estado nuevo). El estado viejo se realimenta como entrada del próximo paso, y en ese lazo de realimentación vive toda la dificultad.

// Sin estado: misma entrada, misma salida, para siempre.
function doble(x: number): number {
  return x * 2;
}
doble(21); // 42, hoy y dentro de un año

// Con estado: la salida depende de algo que no está en los argumentos.
let total = 0;
function acumular(x: number): number {
  total += x;      // muta memoria que sobrevive a la llamada
  return total;    // 5, luego 12, luego 20... misma llamada, otro valor
}
flowchart LR
E[entrada del evento] --> T[funcion de transicion]
S[estado actual] --> T
T --> O[salida]
T --> N[estado nuevo]
N -.realimenta.-> S
style S fill:#f38ba8,color:#11111b
style T fill:#89b4fa,color:#11111b
style N fill:#f38ba8,color:#11111b

Ese diagrama es el esqueleto de todo lo que verás en el track. Un signal es esa función de transición ejecutándose sola cuando cambia una dependencia; un reducer de Redux es la función escrita a mano; una máquina de estado es la misma idea con las transiciones válidas declaradas de antemano. Cambia quién escribe la función y cuánta libertad tiene, pero el esqueleto —entrada más estado producen salida más estado nuevo— no cambia jamás. Aprender estado es aprender a mirar cualquier herramienta y reconocer este esqueleto debajo.

Conviene además distinguir el estado del programa del estado del mundo. Una base de datos, el sistema de archivos, el DOM del navegador: todos guardan memoria que sobrevive a los eventos, y todos son estado desde nuestra óptica aunque vivan fuera de nuestro proceso. Gran parte del arte de este track consiste en decidir qué estado gobierna tu código y qué estado solo refleja —como una cache o una vista— algo cuya verdad vive en otra parte.

¿Es esto estado? El criterio de la persistencia

No todo dato es estado. El criterio no es que la variable exista, sino que sobreviva al evento y condicione el siguiente. Aplicarlo con rigor es el primer hábito del que domina el estado, porque separa el ruido —cálculos temporales— de lo que de verdad hay que gobernar.

🧮

Cálculo intermedio: NO

Una variable local dentro de una función, un acumulador de un bucle, el resultado parcial de un map. Nace y muere en la misma ejecución; nadie la recuerda después. Es andamio, no estado.

💰

Saldo de una cuenta: SÍ

Sobrevive a la transacción que lo modificó y condiciona la próxima: no puedes retirar más de lo que hay. Persiste entre eventos y gobierna la respuesta futura. Estado puro.

🎛️

Posición del scroll: SÍ

Efímero y de UI, pero es estado: recuerda dónde estaba el usuario entre dos renders y condiciona qué se ve. Que sea trivial no lo exime; muchos bugs viven en este estado “menor”.

🔢

El total de un carrito: DEPENDE

Si lo almacenas, es estado (y probablemente accidental). Si lo derivas de los ítems cada vez, no lo es: es un cálculo. La misma cifra puede ser estado o no según cómo la trates.

Esa última tarjeta encierra la primera gran decisión de diseño: mucho de lo que tratamos como estado no tiene por qué serlo. Un valor que puede recalcularse a partir de otros no necesita almacenarse; almacenarlo es convertir un cálculo en un dato que hay que mantener sincronizado. Volveremos a esta idea —derivar en vez de duplicar— porque es el hilo que recorre todo el nivel.

El criterio también revela por qué el estado de UI, tan despreciado, causa tantos fallos. Un desplegable abierto o cerrado, un modal visible u oculto, la pestaña activa: son estado real, con su propio espacio de combinaciones, y rara vez reciben la disciplina que damos a los datos “serios”. La mayoría de las inconsistencias visuales que el usuario reporta como “raras” son, en el fondo, dos piezas de este estado humilde que quedaron en desacuerdo.

Por qué el estado hace difícil razonar

La propiedad que el estado destruye tiene nombre: transparencia referencial. Una expresión es referencialmente transparente si puedes sustituirla por su valor sin cambiar el significado del programa. doble(21) es transparente: allí donde aparezca puedes escribir 42 y nada se altera. Es el modelo de sustitución del que vive el álgebra y toda la programación funcional.

El estado la rompe de raíz. acumular(5) no es un valor: es una pregunta cuya respuesta depende de cuándo la hagas. No puedes sustituirla por su resultado porque su resultado cambia con la historia. Y en el instante en que una sola expresión de tu programa deja de ser sustituible, razonar deja de ser sustituir: pasa a ser simular mentalmente toda la secuencia de eventos que condujo al estado actual.

Hay además una explosión combinatoria. Un programa con n variables booleanas tiene 2^n estados posibles. Entender qué hace una línea exige considerar en cuál de esos estados puede ejecutarse. Añade una cadena mutable y el espacio de estados se vuelve infinito. Las pruebas solo muestrean ese espacio; un bug es, casi siempre, un estado que nadie muestreó.

flowchart TD
A[1 variable booleana] --> B[2 estados]
C[2 variables] --> D[4 estados]
E[3 variables] --> F[8 estados]
G[n variables] --> H[dos elevado a n estados]
style H fill:#f38ba8,color:#11111b
style G fill:#fab387,color:#11111b

El crecimiento es exponencial, y con él la distancia entre lo que crees haber probado y lo que de verdad puede ocurrir. Cada variable mutable que añades no suma un caso: duplica el universo. Por eso la primera defensa contra los bugs de estado no es más pruebas, sino menos estado: recortar variables es podar ramas enteras del árbol de posibilidades.

Por eso el estado es peor que la complejidad algorítmica. Un algoritmo de ordenación difícil sigue siendo total: dada la entrada, su comportamiento está definido y es el mismo siempre. Un programa con estado es difícil y parcial: su comportamiento solo está definido —solo lo entendiste— en los estados que pensaste, y queda en penumbra en el resto. La dificultad no está en la lógica de una línea, sino en el conjunto invisible de mundos en que esa línea puede ejecutarse.

Esta es la raíz de la sensación, familiar para todo programador, de que un sistema “se le fue de las manos”. No es que la lógica se volviera más compleja; es que el espacio de estados creció más rápido que la capacidad de nadie para sostenerlo en la cabeza. El código cabe en la pantalla; sus estados posibles, no. Domar el estado es, ante todo, mantener ese espacio lo bastante pequeño como para poder pensarlo entero.

💡
El test de la sustitución

Para saber si un fragmento depende del estado, pregúntate: ¿puedo reemplazar esta llamada por el valor que devolvió la última vez, sin cambiar el programa? Si la respuesta es sí, es puro y fácil de razonar. Si es no, hay estado en juego, y ese “no” marca exactamente la frontera donde tu código deja de ser predecible.

El estado es la entrada oculta

Mira de nuevo acumular(x). Aparenta tomar un argumento, pero en realidad toma dos: x y el total invisible. Todo programa con estado esconde ese segundo argumento. Es f(x) que en verdad es f(x, s) con s implícito, arrastrado por debajo de la firma.

Esa entrada oculta es la fuente de casi toda la dificultad práctica. Vuelve difícil probar —para reproducir un caso debes reconstruir s—, difícil paralelizar —dos hilos comparten s— y difícil depurar —el bug solo aparece en un s concreto que tienes que recrear a mano—. La cura empieza por sacar s a la luz: convertirlo en un argumento visible y devolver el estado nuevo en vez de mutarlo por debajo.

// La entrada oculta, hecha explícita: un reducer puro.
type Estado = { total: number };
type Evento = { tipo: "sumar"; monto: number } | { tipo: "reiniciar" };

function reducir(estado: Estado, evento: Evento): Estado {
  switch (evento.tipo) {
    case "sumar":     return { total: estado.total + evento.monto };
    case "reiniciar": return { total: 0 };
  }
}
// Ahora s es un argumento a la vista: mismo (estado, evento), mismo resultado.

Con s explícito recuperas la transparencia referencial: reducir vuelve a ser sustituible, testeable sin montar un mundo, y trazable evento a evento. Toda la disciplina de este track —signals, inmutabilidad, Flux, Elm, TCA— es, en el fondo, una variación de este mismo gesto: domar el s oculto haciéndolo explícito, pequeño y controlado en vez de implícito, enorme y disperso.

Hay un premio inesperado en esta forma. Si el estado nuevo es siempre función del estado viejo y un evento, entonces la secuencia de eventos es el programa: guardando los eventos puedes reconstruir cualquier estado pasado reproduciéndolos desde el principio. Eso es el event sourcing, y es también lo que hace posible el time-travel debugging de Redux DevTools. Nada de eso es un truco de librería: cae de gratis en cuanto tratas el estado como el resultado de una historia explícita en vez de como una casilla que se sobrescribe.

ℹ️
De la mutación a la historia

Cuando escribes total += x destruyes el pasado: el valor anterior desaparece sin dejar rastro. Cuando escribes reducir(estado, evento) conservas la posibilidad de la historia, porque el estado viejo sigue existiendo y el nuevo es un valor aparte. La diferencia entre sobrescribir y transformar es la diferencia entre un programa que olvida y uno que recuerda cómo llegó a donde está —y solo el segundo se puede auditar.

El estado es el tiempo hecho dato

Un programa sin estado vive en un presente eterno: no hay antes ni después, solo entradas y salidas que se corresponden como una tabla matemática. El estado inyecta la flecha del tiempo dentro del programa. De pronto el orden importa, el antes y el después se vuelven distinguibles, y la misma acción tiene consecuencias diferentes según el pasado acumulado. Programar sin estado es álgebra; programar con estado es historia. Y la historia tiene una propiedad incómoda: no se puede resumir mirando la última línea. Para saber dónde estás debes saber cómo llegaste. Ese es el impuesto que cobra el estado, y todo lo que aprenderás en este track —desde el signal más simple hasta el CRDT más sofisticado— es una forma distinta de pagarlo con conciencia en vez de sufrirlo por accidente. Quien entiende que el estado es tiempo materializado dentro de la máquina deja de ver los bugs de estado como mala suerte y empieza a verlos como lo que son: consecuencias inevitables de una historia que nunca controló del todo.

⚔️ Detecta el estado escondido
  1. Toma una función de tu código que devuelva cosas distintas con los mismos argumentos. Identifica su entrada oculta s: ¿dónde vive, quién la muta, quién la lee?
  2. Reescríbela como transductor explícito, al estilo del reducir de arriba: que reciba el estado y devuelva el estado nuevo, sin tocar nada de fuera. ¿Se volvió más fácil de probar?
  3. Cuenta las variables mutables de un módulo tuyo. Estima su espacio de estados —2^n para n booleanos—. ¿Cuántos de esos estados has probado de verdad?
  4. Busca en tu último bug difícil la frase “solo pasa a veces”. Casi siempre esconde un estado concreto que no habías considerado. Nómbralo y descríbelo.