wandres.dev
DERIVACIÓN · memos y selectores

Derivación en cascada: el grafo y su propagación

Cuando un memo depende de otro memo, las derivaciones dejan de ser puntos aislados y forman un grafo dirigido acíclico. Propagar un cambio por ese grafo tiene dos trampas: el glitch, donde un nodo se recalcula con entradas incoherentes o más de una vez, y el sobrecoste de recomputar ramas que no cambiaron. La reactividad de grano fino de 2026 las resuelve con propagación sin glitches, evaluación perezosa por pull y corte por igualdad.

⏱ 18 min

Las derivaciones rara vez viven solas. El total de un carrito deriva de las líneas visibles, que a su vez derivan de las líneas y el filtro; una etiqueta de resumen deriva del total y del recuento. En cuanto un memo depende de otro memo, las derivaciones dejan de ser puntos aislados y se organizan en un grafo dirigido acíclico: las señales fuente en las raíces, los memos en los nodos intermedios, y los efectos y la UI en las hojas. Propagar un cambio de una raíz hasta las hojas parece trivial, pero esconde dos problemas que definen la calidad de un sistema reactivo: el glitch, donde un nodo se recalcula con entradas incoherentes o más veces de las debidas, y el sobrecoste, donde se recomputan ramas enteras que en realidad no cambiaron. La reactividad de grano fino de 2026 existe, en buena medida, para resolver ambos.

🎯 Al terminar esta lección sabrás
  • Modelar un conjunto de memos dependientes como un grafo dirigido acíclico de derivación.
  • Reconocer el glitch —recálculo con entradas incoherentes o repetido— y por qué el diamante lo provoca.
  • Entender la propagación sin glitches por orden topológico y las fases de marcado y pull perezoso.
  • Aprovechar el corte por igualdad para que un cambio muera antes de llegar a las hojas.

Memos sobre memos: el grafo de derivación

Cuando declaras un memo que lee otros memos, estás dibujando aristas en un grafo sin saberlo. Cada derivación es un nodo; cada lectura de otra señal o memo dentro de su cuerpo es una arista dirigida desde la dependencia hacia el dependiente. El resultado es un grafo dirigido y acíclico —un ciclo significaría que un valor depende de sí mismo, y los sistemas reactivos lo prohíben o lo detectan como error—, con las fuentes mutables en las raíces y la UI en las hojas. La reactividad de grano fino de Solid, de las señales del TC39, de Angular, de Vue y de Preact no es más que la construcción y el mantenimiento automáticos de este grafo: tú escribes derivaciones, y el runtime descubre las aristas rastreando qué se lee durante cada evaluación.

const items = createSignal([]);                 // raiz: estado fuente
const filtro = createSignal("activos");          // raiz: estado fuente
const visibles = createMemo(() => items().filter(coincide(filtro())));       // nodo 1
const total = createMemo(() => visibles().reduce((s, i) => s + i.precio, 0)); // nodo 2 sobre el 1
const etiqueta = createMemo(() => `${visibles().length} por ${moneda(total())}`); // nodo 3 sobre 1 y 2

El mismo grafo se construye en el mundo de Redux por composición de selectores: un createSelector puede tomar como selector de entrada a otro selector memoizado, encadenando derivaciones en las que cada eslabón cachea su parte. La diferencia es de grano y de automatismo —reselect compone a mano lo que las señales tejen solas—, pero la estructura es idéntica: un grafo por el que un cambio en la raíz debe propagarse hasta donde importe, y solo hasta donde importe.

// Redux: composicion de selectores memoizados es tambien un grafo de derivacion
const selectVisibles = createSelector([selectItems, selectFiltro], filtrar);
const selectTotal = createSelector([selectVisibles], sumarPrecios); // memo sobre memo

El glitch y el problema del diamante

El grafo tiene una topología peligrosa: el diamante. Una raíz a alimenta a dos memos b y c, y un cuarto memo d depende de ambos. Cuando a cambia, una propagación ingenua que empuje el cambio en cuanto lo ve puede recalcular d la primera vez que b se actualiza —cuando c todavía refleja el valor viejo de a— y luego otra vez cuando c se actualiza. Ese primer cálculo de d mezcla un b nuevo con un c viejo: es un estado incoherente, transitorio y observable, que se conoce como glitch. Y aunque no se llegara a observar, recalcular d dos veces por un solo cambio de a es trabajo desperdiciado.

// Diamante: a alimenta a b y a c; d se reune y depende de ambos
const a = signal(1);
const b = computed(() => a.value + 1);        // rama izquierda
const c = computed(() => a.value * 2);        // rama derecha
const d = computed(() => b.value + c.value);  // debe evaluarse UNA vez, con b y c coherentes
flowchart TD
A[senal fuente a] --> B[memo b rama izquierda]
A --> C[memo c rama derecha]
B --> D[memo d se reune aqui]
C --> D
style A fill:#89b4fa,color:#11111b
style D fill:#a6e3a1,color:#11111b
ℹ️
Un glitch es un estado incoherente que nunca debió ser observable

La palabra viene de la electrónica: una salida que oscila a un valor espurio antes de estabilizarse. En reactividad, un glitch es el instante en que un memo se calcula con una mezcla de entradas de generaciones distintas —unas ya actualizadas por el cambio en curso, otras aún no—. Un sistema sin glitches garantiza que ningún nodo se evalúe hasta que todas sus dependencias reflejen el mismo cambio, de modo que cada actualización se ve como una transición atómica del grafo entero, sin estados intermedios que no correspondan a ninguna verdad.

Propagación eficiente: pull perezoso y corte por igualdad

La solución que comparte el ecosistema moderno es una propagación en dos fases que evita a la vez el glitch y el recálculo múltiple. La primera fase es un empuje ligero: cuando una raíz cambia, no recalcula nada, solo recorre el grafo marcando a sus descendientes como sucios o posiblemente sucios, coloreándolos sin ejecutar sus cuerpos. La segunda fase es un pull perezoso: cuando alguien lee de verdad un nodo —un efecto, la UI—, el runtime asciende por sus dependencias y recalcula, en orden topológico, solo los nodos realmente sucios, garantizando que cuando le toca a d tanto b como c ya están al día. Así cada nodo se evalúa como mucho una vez por actualización y siempre con entradas coherentes, y los nodos que nadie observa no se calculan en absoluto.

Estrategia Cuándo calcula Riesgo que evita
Push ingenuo al vuelo, en cuanto ve el cambio ninguno: sufre glitches y recálculo doble
Pull perezoso puro al leerse, si está sucio no calcula lo que nadie observa
Push de marcado y pull marca al cambiar, calcula al leer glitches y recálculo múltiple, a la vez

Sobre esa base actúa la palanca que hace baratas las cascadas largas: el corte por igualdad. Cuando un memo se recalcula y su nuevo valor resulta igual al anterior —según Object.is o una función de igualdad que tú provees—, el runtime no marca como sucios a sus consumidores: la propagación muere ahí. Esto significa que un cambio en una raíz no tiene por qué llegar hasta las hojas; si un memo intermedio absorbe el cambio y produce el mismo resultado, todo el subgrafo que cuelga de él se ahorra el recálculo. Solid lo expone con la opción equals de createMemo, Angular y Vue lo aplican en sus computed, y las señales del TC39 y de Preact lo integran en su algoritmo de versiones. Diseñar buenas funciones de igualdad en los nodos intermedios es, por tanto, una herramienta de rendimiento tan potente como la memoización misma.

💡
Coloca el corte por igualdad donde el grafo se estrecha

Un memo intermedio que normaliza o reduce —convierte una lista enorme en un recuento, un objeto complejo en un booleano— es el lugar ideal para que la propagación se detenga. Si su salida cambia raras veces aunque su entrada cambie a menudo, un corte por igualdad en ese punto protege a todo el subgrafo que tiene debajo. Buscar esos cuellos de botella naturales del grafo y asegurarse de que comparan bien rinde mucho más que optimizar las hojas una a una.

Push o pull: cómo propaga cada ecosistema

Los sistemas reactivos se reparten en dos filosofías de propagación, y conocerlas explica el comportamiento de cada librería. En la propagación push, un cambio en una raíz empuja notificaciones hacia adelante y actualiza a los dependientes de inmediato; es ansiosa y de baja latencia, pero propensa a glitches y a recomputar nodos que nadie observa. En la propagación pull, nada se calcula hasta que alguien lee un valor, momento en que el sistema tira de las dependencias hacia atrás; es perezosa y evita todo trabajo no observado, pero necesita saber qué está sucio para no recalcular de más.

El ecosistema de 2026 converge en un híbrido push-pull: una fase push ligera que solo marca el subgrafo como sucio, y una fase pull que recalcula perezosamente al leer, combinando la coherencia de la primera con la economía de la segunda. Cada framework lo implementa con su propia contabilidad de versiones o de coloreado, pero la semántica observable es idéntica: sin glitches y sin recálculo redundante.

📥

Pull perezoso

Solid, Vue, Angular y las señales del TC39 no calculan hasta que se lee. Lo que nadie observa no cuesta, y la coherencia se garantiza al ascender por las dependencias.

📤

Push ansioso

Los observables al estilo RxJS empujan valores en cuanto llegan. Máxima inmediatez, a cambio de orquestar a mano el orden y la deduplicación de las emisiones.

🔀

Híbrido push-pull

Marcar sucio al cambiar y recalcular al leer. Es el diseño que comparten los memos de grano fino modernos: coherente como el pull, barato como el marcado.

Sistema Estrategia Cómo detecta el cambio
Solid push-pull de grano fino marcado por nodo y opción equals
Vue y Angular pull con versiones versión de dependencia y corte
Preact y Signals TC39 push-pull con versiones contador global de versión
MobX pull con derivaciones seguimiento de lo observado

Un matiz que sorprende: el grafo no es estático. Como las dependencias se descubren ejecutando el cuerpo del memo, una rama condicional puede cambiar qué se lee y, por tanto, las aristas del grafo de una evaluación a otra. Un memo que lee a cuando una bandera está activa y b cuando no, deja de depender de b en cuanto la bandera cambia, y el sistema reajusta las suscripciones solo. Esta dependencia dinámica es lo que hace tan precisa a la reactividad de grano fino: nunca te suscribes a más de lo que de verdad lees en cada evaluación.

// Dependencia dinamica: el grafo cambia segun que rama se ejecute
const vista = createMemo(() => (modoDetalle() ? detalle() : resumen()));
// Cuando modoDetalle es falso, este memo ni siquiera depende de detalle

Ese ajuste automático de suscripciones tiene un coste que conviene no disparar sin querer. Cuando cambias varias raíces en sucesión, cada escritura puede iniciar su propia pasada de propagación, de modo que un memo compartido aguas abajo se recalcula una vez por cada raíz que tocaste, aunque el resultado final sea el mismo. El batching agrupa esas escrituras en una sola transacción para que el grafo se recalcule una única vez y de forma coherente.

// Batching: varias escrituras se agrupan en una sola propagacion coherente
batch(() => {
  primero.set(10);
  segundo.set(20);
}); // un memo que depende de ambos se recalcula una vez, no dos veces

En las hojas del grafo viven los efectos, los únicos nodos que no derivan un valor sino que provocan un efecto secundario —pintar, escribir en el DOM, lanzar una petición— cuando sus dependencias cambian. A diferencia de los memos, los efectos son ansiosos: se reejecutan en cuanto la propagación termina, y son el punto donde el grafo puro de derivación se conecta con el mundo exterior. Colocar la lógica de derivación en memos y reservar los efectos para lo verdaderamente externo mantiene el grafo limpio y su propagación predecible.

// Un efecto es una hoja ansiosa del grafo: reacciona, no deriva
createEffect(() => { document.title = `${visibles().length} elementos`; });
💡
Agrupa las escrituras para que la propagación ocurra una sola vez

Si cambias varias raíces seguidas, cada cambio podría disparar su propia propagación y recalcular un memo compartido una vez por escritura. La solución que ofrecen todos estos sistemas es el batching —batch en Solid, las transacciones de MobX, el ciclo de actualización de Vue y Angular—: agrupa las escrituras en una única transacción para que el grafo se recalcule una sola vez, coherente, al final. Es el reverso del corte por igualdad: en lugar de detener la propagación, la unifica.

El grafo de derivación convierte la coherencia y el mínimo trabajo en propiedades estructurales

La lección que trasciende cualquier librería de señales es que, en cuanto aceptas derivar en lugar de guardar, tu programa deja de ser una secuencia de instrucciones y se vuelve un grafo de dependencias que un runtime mantiene coherente por ti; y las dos preguntas difíciles de toda reactividad —cómo evitar estados incoherentes y cómo no hacer trabajo de más— dejan de ser tu responsabilidad manual para convertirse en propiedades del grafo. Piénsalo en contraste con las dos alternativas ingenuas, porque el grafo es precisamente lo que las supera. Podrías recalcular todo lo derivado ante cualquier cambio: eso es correcto pero derrocha, recomputa ramas que nadie tocó y ramas que nadie mira. O podrías rastrear a mano qué depende de qué y actualizar solo lo justo: eso es eficiente pero frágil, porque un dependiente olvidado produce datos obsoletos, el peor de los bugs de estado, silencioso y plausible. El grafo de derivación es la síntesis que se queda con lo bueno de ambas: es tan correcto como recalcular todo, porque el orden topológico garantiza que nadie se evalúe con entradas de generaciones distintas y que cada nodo se calcule a lo sumo una vez; y es tan eficiente como el rastreo manual, porque el marcado perezoso solo toca lo que de verdad cambió y el corte por igualdad detiene la propagación en cuanto un valor se estabiliza, dejando intacto todo lo que cuelga debajo. Lo profundo es que esta maquinaria invierte la carga de la prueba: en el modelo imperativo, mantener coherente el estado derivado era una obligación que recaía sobre tu disciplina y se rompía en el caso que olvidaste; en el modelo del grafo, la coherencia y el mínimo recálculo son consecuencias automáticas de haber declarado las dependencias correctamente, que el runtime descubre solo al ver qué lees. Por eso la reactividad de grano fino no es una moda de sintaxis, sino un cambio en quién garantiza qué: tú declaras la forma del grafo escribiendo derivaciones puras, y a cambio recibes, gratis, la garantía de que ningún consumidor verá jamás una mezcla incoherente ni pagará por un cálculo que no cambió su resultado.

⚔️ Piensa en grafos, no en cadenas
  1. Dibuja el grafo de derivación de un componente real: marca las raíces, los memos intermedios y las hojas, y señala dónde aparece un diamante.
  2. Construye un diamante mínimo con las señales de tu framework y añade un contador de evaluaciones a cada nodo; provoca un cambio y verifica que ningún nodo se calcula más de una vez.
  3. Encadena tres memos y comprueba que el más lejano de la raíz no se recalcula cuando nadie lo lee, confirmando la evaluación perezosa.
  4. Añade un memo intermedio que reduzca a un booleano y dale una buena igualdad; demuestra que un cambio en la raíz que no altera ese booleano no propaga a las hojas.
  5. Traslada el mismo grafo a Redux componiendo selectores memoizados y contrasta cuánto de la propagación tienes que orquestar a mano frente a lo que las señales resuelven solas.