Componer derivaciones: el grafo de memos
Memos que dependen de memos forman un grafo acíclico de derivaciones. Solid lo propaga en orden topológico y sin glitches: el problema del diamante se resuelve con una sola recomputación de cada nodo y ningún consumidor ve estados intermedios.
Un memo puede leer otros memos. Encadenados, forman un grafo acíclico dirigido de derivaciones: fuentes abajo, valores compuestos arriba. La pregunta difícil no es cómo construirlo —basta leer un memo dentro de otro— sino cómo Solid lo actualiza sin que ningún consumidor llegue a ver, ni por un instante, una combinación imposible de valores. Esa propiedad se llama propagación glitch-free, y es el corazón de la reactividad de grano fino.
- Componer memos que dependen de otros memos.
- Entender el grafo de derivación como un DAG.
- Comprender la propagación en orden topológico y glitch-free.
- Ver cómo el batching agrupa escrituras en una sola estabilización.
Memos sobre memos
Componer es leer un memo dentro de otro. Cada capa depende de la anterior y añade significado:
const [nombre, setNombre] = createSignal("Ada");
const [apellido, setApellido] = createSignal("Lovelace");
const completo = createMemo(() => `${nombre()} ${apellido()}`);
const inicial = createMemo(() => completo()[0].toUpperCase());
const saludo = createMemo(() => `Hola, ${completo()}`);
inicial y saludo dependen de completo, que depende de dos signals. Nadie orquesta el orden a mano: cada memo se suscribe a lo que lee, y el grafo se dibuja solo. Cuando cambie nombre, la actualización fluirá nombre → completo → { inicial, saludo } sin que tú escribas una línea de coordinación. Esta composición es el análogo reactivo de la composición de funciones puras: pequeñas derivaciones que se apilan en otras mayores, cada una nombrando un concepto.
El grafo debe ser acíclico: un memo no puede depender, directa o indirectamente, de sí mismo. Si a lee b y b lee a, no existe orden topológico posible y el sistema no sabría por dónde empezar. Casi nunca escribes un ciclo a propósito, pero se cuela por accidente cuando un memo lee un signal que un efecto suyo escribe; se rompe moviendo esa escritura fuera o replanteando quién deriva de quién.
Componer no tiene límite práctico de profundidad: memos sobre memos sobre memos forman capas de significado, y el coste de propagar por ellas es lineal en las que de verdad cambian, no en el tamaño total del grafo. Un cambio en una hoja lejana no despierta a ramas que no dependen de ella.
flowchart TD N[signal nombre] --> C[memo completo] A[signal apellido] --> C C --> I[memo inicial] C --> S[memo saludo] I --> V1[vista] S --> V2[vista] style N fill:#89b4fa,color:#11111b style A fill:#89b4fa,color:#11111b style C fill:#cba6f7,color:#11111b style I fill:#cba6f7,color:#11111b style S fill:#cba6f7,color:#11111b
El problema del diamante
Considera un diamante: una fuente A alimenta a B y a C, y un nodo D lee a los dos. Si A cambia, ¿cuántas veces debe recomputar D? La respuesta correcta es una, y solo después de que B y C estén ya actualizados. Un sistema ingenuo fallaría de dos maneras: recomputando D dos veces —una por B, otra por C— o, peor, recomputándolo cuando B ya es nuevo pero C sigue viejo. Ese estado transitorio incoherente es un glitch.
flowchart TD A[signal A] --> B[memo B] A --> C[memo C] B --> D[memo D] C --> D style A fill:#89b4fa,color:#11111b style B fill:#cba6f7,color:#11111b style C fill:#cba6f7,color:#11111b style D fill:#fab387,color:#11111b
const [a, setA] = createSignal(1);
const b = createMemo(() => a() + 1);
const c = createMemo(() => a() * 2);
const d = createMemo(() => b() + c()); // debe verse consistente SIEMPRE
La raíz del glitch es propagar en el acto: si al cambiar A avisas a B y B avisa a D antes de que C se entere, D corre con un C viejo. Solid lo evita no propagando en caliente, sino marcando como sucios a los descendientes y dejando que la lectura ordene el recálculo. Ese aplazamiento es, contra toda intuición, lo que produce inmediatez correcta. El diamante es el caso mínimo del problema, pero la misma lógica escala a grafos de cientos de nodos sin que aparezca un solo estado incoherente.
Cómo Solid lo garantiza
Solid propaga en orden topológico: cada nodo se recomputa solo cuando todas sus fuentes ya son válidas. El mecanismo combina empuje y tirón. Al escribir un signal, Solid marca como potencialmente sucios a sus descendientes —es la fase de empuje, que baja por el grafo—. Luego, al leer un nodo, este tira de sus fuentes: si alguna está sucia, la recomputa primero y comprueba si de verdad cambió. Así d, al leerse, obliga a b y c a estar frescos antes de sumar, y como cada memo cachea, ninguno se recalcula dos veces en el mismo tick.
Fíjate en que esta garantía reutiliza las dos propiedades de las lecciones anteriores: el orden topológico es posible porque cada memo, por su doble naturaleza, sabe quiénes son sus fuentes y quiénes sus observadores; y “una sola vez” es posible porque el memo cachea. Componer no añade un mecanismo nuevo: hace colaborar los que ya tenías.
Sigue el tirón paso a paso sobre una cadena lineal:
const [a, setA] = createSignal(1);
const doble = createMemo(() => a() * 2);
const cuadruple = createMemo(() => doble() * 2);
setA(5); // marca doble y cuadruple como sucios; nadie corre aun
cuadruple(); // tira: valida doble (recomputa a 10) y luego cuadruple (20)
// cada nodo se recalcula una sola vez, de arriba abajo, en el orden correcto
Este es el mecanismo push-pull: el empuje marca barato —solo pone banderas de “sucio”— y el tirón, al leer, hace el trabajo caro —recomputar— exactamente donde y cuando hace falta. Separar el marcar del recomputar es justo lo que permite que muchas señales cambien y el grafo siga recalculando cada nodo una única vez.
Nada de esto lo programas tú: emerge de que cada memo conoce sus fuentes y sus observadores. La topología es un subproducto de la doble naturaleza del memo, no una estructura que tengas que declarar ni mantener a mano.
Las dependencias se descubren solas al leer, pero a veces quieres controlarlas. untrack lee un signal sin suscribirte a él —útil cuando un memo necesita un valor puntual que no debe reactivarlo—. on declara dependencias explícitas y ejecuta el cálculo solo cuando ellas cambian, ignorando lo demás que leas dentro. Con estas dos herramientas dejas de sufrir el grafo que Solid infiere y empiezas a diseñarlo: qué aristas existen, cuáles no, y qué fuente manda el recálculo.
Batching: muchas escrituras, una estabilización
¿Qué pasa si cambias dos fuentes seguidas? Sin coordinación, cada escritura dispararía su propia oleada y los nodos compartidos correrían de más. Solid lo evita con batching: dentro de un manejador de eventos, las escrituras se agrupan y el grafo se estabiliza una sola vez al final.
import { batch } from "solid-js";
batch(() => {
setNombre("Grace");
setApellido("Hopper");
});
// completo se recomputa UNA vez, ya con ambos valores nuevos,
// no una por setNombre y otra por setApellido.
Los manejadores de eventos ya baten por defecto, así que rara vez llamas a batch a mano; lo usas cuando disparas varias escrituras fuera de un evento —en un setTimeout, en una respuesta de red— y quieres una única estabilización coherente. El resultado es el mismo principio que el diamante lleva al extremo: por muchas fuentes que muevas a la vez, el grafo se asienta una sola vez, sin estados intermedios visibles.
Un ejemplo aplicado lo aterriza. Piensa en un formulario cuyo botón de envío depende de varias reglas derivadas:
const [email, setEmail] = createSignal("");
const [pass, setPass] = createSignal("");
const emailOk = createMemo(() => /.+@.+/.test(email()));
const passOk = createMemo(() => pass().length >= 8);
const puedeEnviar = createMemo(() => emailOk() && passOk());
// Rellenar ambos campos de golpe estabiliza el grafo una vez:
batch(() => { setEmail("a@b.co"); setPass("secreto12"); });
// emailOk, passOk y puedeEnviar se asientan juntos; el boton no parpadea.
Sin batch, escribir los dos campos dispararía dos oleadas y puedeEnviar recomputaría dos veces, pudiendo pasar por un estado intermedio incoherente según el orden. Con batch, el grafo ve ambos valores nuevos a la vez y puedeEnviar se resuelve una sola vez, ya correcto.
Interiorizado esto, componer deja de dar respeto: puedes construir cadenas de derivación arbitrariamente hondas sabiendo que Solid las mantendrá coherentes y mínimas en cada actualización, sin que tú vigiles el orden ni cuentes recomputaciones.
Un sistema reactivo que dejara ver estados intermedios sería inutilizable: tu UI parpadearía con combinaciones imposibles, tus efectos dispararían peticiones basadas en datos a medio actualizar, tus cálculos verían un b nuevo junto a un c viejo. Por eso glitch-free no es un lujo de rendimiento, sino la propiedad que hace que el grafo signifique algo. Solid la garantiza con piezas que ya conoces sueltas y ahora ves colaborar: el orden topológico —consecuencia de la doble naturaleza del memo—, el cacheo —una ejecución por cambio— y el batching —una estabilización por lote—. Juntas aseguran que, en cualquier instante en que tu código lea un valor, todo el subgrafo del que depende ya está actualizado y es mutuamente consistente. No hay ventana temporal donde acechen los glitches. Componer derivaciones en Solid es, por tanto, seguro por construcción: apila memos sobre memos con la confianza de que el sistema nunca te mostrará una mentira transitoria.
- Reproduce el diamante
a → b, a → c, d = b + c. Añade unconsole.logendcon los valores deb()yc(). - Cambia
auna vez. Confirma quedse computa una sola vez y que elby elcque ve son ambos nuevos, nunca uno viejo. - Envuelve dos escrituras en
batchy verifica con logs que el memo de aguas abajo corre una vez, no dos. - Dibuja en papel el DAG de un formulario real y marca qué nodos son fuentes, cuáles memos intermedios y cuáles hojas.
- Intenta crear un ciclo a propósito —un memo que dependa de sí mismo vía un efecto que escribe su fuente— y observa cómo el sistema se queja o entra en bucle. Luego rómpelo.