wandres.dev
SIGNALS · el modelo y TC39

Valores computados

Un computed es un signal que no escribes sino que derivas: una expresión de otros signals que se cachea y solo se recalcula cuando una de sus fuentes cambia de verdad. Pereza, marcado sucio, el modelo push-pull y el problema del diamante.

⏱ 16 min

Casi ningún estado interesante es primitivo; la mayoría es derivado. El total de un carrito es precio por cantidad; el nombre completo es nombre más apellido; la lista visible es la lista filtrada por el término de búsqueda. Un valor computado captura esa idea: un signal cuyo valor no lo escribes tú, sino que lo calcula una función de otros signals. Y lo hace con dos virtudes que lo separan de una simple función: se acuerda del resultado y no vuelve a calcularlo mientras sus fuentes no cambien. Es, a la vez, consumidor de signals y productor para otros.

🎯 Al terminar esta lección sabrás
  • Definir un computed como una derivación cacheada de otros signals.
  • Comprender la pereza: no recalcula hasta que alguien lo lee.
  • Entender el modelo push-pull de marcado sucio y recálculo bajo demanda.
  • Reconocer el problema del diamante y cómo se evitan los glitches.

Una derivación con memoria

Un computed envuelve una función de cálculo. La primera vez que lo lees, ejecuta esa función, guarda el resultado en una caché y lo devuelve. Mientras tanto, se comporta como un efecto: al ejecutar la función rastrea qué signals leyó y se vuelve dependiente de ellos. Y se comporta como un signal: otros pueden leerlo y volverse dependientes suyos. Esa doble naturaleza es la clave —un nodo intermedio en el grafo, con fuentes arriba y consumidores abajo.

function computado<T>(calcular: () => T) {
  let cache: T;
  let sucio = true;                 // hay que recalcular en la proxima lectura
  const dependientes = new Set<Efecto>();

  const leer = (): T => {
    if (activo) dependientes.add(activo);   // soy leido: registro a mi consumidor
    if (sucio) {
      cache = calcular();           // recomputo solo si estoy sucio
      sucio = false;
    }
    return cache;
  };

  // cuando una fuente cambia, me marco sucio y aviso hacia abajo
  const invalidar: Efecto = () => {
    if (sucio) return;              // ya estaba sucio: no repito el aviso
    sucio = true;
    for (const d of dependientes) d();
  };

  return { leer, invalidar };
}

Este esbozo omite el cableado que conecta invalidar a las fuentes —ese es el rastreo de la lección anterior—, pero muestra lo esencial: un flag sucio, una caché y la regla de recomputar solo cuando de verdad hace falta.

Y como un computed es a la vez lector y fuente, se encadena: un computed puede depender de otro, que depende de otro, formando un grafo de derivaciones de cualquier profundidad. El cambio en la raíz se propaga por toda la cadena, y cada eslabón cachea su propio resultado:

const [nombre, ponerNombre] = signal("Ada");
const [apellido, ponerApellido] = signal("Lovelace");
const completo = computado(() => `${nombre()} ${apellido()}`);
const saludo = computado(() => `Hola, ${completo.leer()}`);  // un computed de otro computed

Pereza: no calcula hasta que lo miras

La palabra clave es pereza. Cuando una fuente cambia, el computed no se recalcula de inmediato; solo se marca sucio. El cálculo real se pospone hasta que alguien lee el computed. Si nadie lo lee, no se paga nada, por muchas veces que cambien sus fuentes. Esta demora tiene una consecuencia práctica enorme: puedes definir cientos de derivaciones sin coste, porque solo las que de verdad se observan llegan a ejecutarse.

const [precio, ponerPrecio] = signal(100);
const [cantidad, ponerCantidad] = signal(2);
const total = computado(() => precio() * cantidad());

total.leer();          // primera lectura: calcula 200 y lo cachea
total.leer();          // segunda lectura: devuelve 200 de cache, no recalcula
ponerCantidad(3);      // marca total sucio, pero NO recalcula todavia
total.leer();          // ahora si: recalcula 300 bajo demanda
💡
Computed frente a función normal

Podrías escribir total como una función que multiplica en cada llamada. Funcionaría, pero recalcularía siempre, aunque nada haya cambiado, y no avisaría a nadie cuando el resultado varíe. El computed añade dos cosas que la función pura no tiene: memoización sensible a las fuentes —no repite trabajo— e integración en el grafo —notifica hacia abajo—. Regla rápida: si la derivación es cara o si otros deben reaccionar a ella, computed; si es trivial y de usar y tirar, una función basta.

📝
El efecto es el hermano ávido del computed

La pereza tiene una excepción deliberada: el efecto. Un computed es perezoso porque produce un valor que quizá nadie mire, así que puede esperar a ser leído. Un efecto existe para provocar un cambio en el mundo —pintar en la pantalla, escribir en la red, mover el foco— y por eso debe ejecutarse aunque nadie lo lea. Computed y efecto son las dos caras de la reactividad derivada: el primero es perezoso y puro, el segundo es ávido y con efectos secundarios. Todo nodo intermedio del grafo es, en el fondo, una combinación de estos dos temperamentos.

Push-pull: empujar el aviso, tirar del valor

El modelo moderno de computed es un híbrido de dos estrategias clásicas. La invalidación se empuja: cuando una fuente cambia, recorre el grafo hacia abajo marcando sucios a sus dependientes —un push barato que solo propaga un bit, no valores—. El recálculo se tira: ocurre cuando alguien lee, subiendo por el grafo para pedir los valores que necesita —un pull que solo se paga si de verdad se observa—. Push del aviso, pull del valor: lo mejor de ambos mundos.

flowchart TD
P[precio cambia] -->|push marca sucio| T[total queda sucio]
Q[cantidad cambia] -->|push marca sucio| T
V[la vista lee total] -->|pull pide el valor| T
T --> R[recomputa una vez y cachea]
style T fill:#cba6f7,color:#11111b
style R fill:#a6e3a1,color:#11111b
style V fill:#89b4fa,color:#11111b
⬇️

Push: el aviso

Cuando una fuente cambia, un bit de suciedad baja por el grafo marcando dependientes. Es barato: propaga que algo cambió, no el valor nuevo.

⬆️

Pull: el valor

Solo al leer un computed se sube por el grafo pidiendo los valores frescos que hagan falta. El cálculo se paga únicamente si alguien observa.

🗃️

Caché: la memoria

Entre un cambio y el siguiente, el resultado vive en caché. Mil lecturas sin cambios cuestan una sola evaluación.

El problema del diamante y los glitches

Considera un grafo en rombo: un signal a, dos computados b y c que dependen de a, y un cuarto nodo d que depende de b y de c. Si cambias a y propagas de forma ingenua y ávida, d podría recalcularse dos veces —una por b y otra por c— y, peor aún, ejecutarse un instante con un b nuevo y un c todavía viejo. Ese estado intermedio incoherente que nunca debió observarse tiene nombre: glitch.

flowchart TD
A[signal a] --> B[computed b]
A --> C[computed c]
B --> D[computed d lee b y c]
C --> D
style A fill:#89b4fa,color:#11111b
style D fill:#a6e3a1,color:#11111b
const [a, ponerA] = signal(1);
const b = computado(() => a() + 1);
const c = computado(() => a() * 2);
const d = computado(() => b.leer() + c.leer());  // depende de dos caminos desde a

ponerA(5);        // un push ingenuo recalcularia d dos veces y lo veria incoherente
d.leer();         // el modelo perezoso lo calcula UNA vez, con b y c ya frescos
ℹ️
Por qué la pereza mata los glitches

El modelo push-pull evita el glitch casi por accidente. Como el push solo marca sucio y no recalcula, cuando a cambia tanto b como c quedan sucios y d queda sucio, pero nadie calcula nada aún. Cuando por fin lees d, este tira de b y de c, que a su vez tiran de a; ambos se recomputan sobre el mismo valor coherente de a y d se evalúa una sola vez sobre resultados frescos. La demora, que parecía un truco de rendimiento, resulta ser también la garantía de consistencia: nunca observas un estado a medio actualizar.

El computed convierte el estado en una función pura de sus fuentes

La lección profunda del valor computado es que casi todo el estado de una aplicación debería ser derivado, no almacenado. Cada dato que guardas a mano es un dato que puede quedar desincronizado de aquello de lo que dependía; cada derivación que expresas como computed es una verdad que, por construcción, jamás puede mentir, porque se recalcula desde su fuente en cuanto la fuente cambia. Reducir el estado primitivo al mínimo irreductible —las pocas cosas que de verdad no se pueden derivar de nada más— y expresar todo lo demás como computados encima es el equivalente reactivo de normalizar una base de datos: eliminas la redundancia y, con ella, la clase entera de bugs en que dos copias de la misma información discrepan. El computed logra esto con tres propiedades que actúan juntas. Es cacheado, así que la elegancia de derivarlo todo no cuesta rendimiento: cada valor se calcula como mucho una vez por cambio relevante. Es perezoso, así que las derivaciones que nadie observa no cuestan nada, y puedes permitirte declararlas con generosidad. Y es consistente, porque el modelo push-pull garantiza que cuando lees un computed obtienes un valor coherente con el estado actual de todas sus fuentes, nunca un glitch a medio propagar. Interioriza esto y tu forma de diseñar cambia: dejas de preguntarte cuándo actualizar este dato derivado —la pregunta que genera los bugs de sincronización— y empiezas a preguntarte de qué es función este dato. Respondida esa pregunta, el sistema mantiene la respuesta al día para siempre. El estado deja de ser un conjunto de variables que empujas a mano y se vuelve un grafo de derivaciones que se autosostiene: tú declaras las relaciones y la reactividad se encarga del resto.

⚔️ Derívalo todo
  1. Implementa computado con caché y flag sucio; comprueba que dos lecturas seguidas sin cambios no recalculan.
  2. Encadena dos computados —uno que dependa de otro— y observa cómo un cambio en la fuente se propaga hasta el final.
  3. Construye el rombo a, b, c, d y cuenta cuántas veces se ejecuta el cálculo de d al cambiar a.
  4. Reemplaza el computed por una función normal y enumera con precisión qué dos propiedades pierdes.
  5. Toma un estado de una app tuya que guardes a mano y reescríbelo como un computed de datos más primitivos.