wandres.dev
LOTES Y PLANIFICACIÓN · Batching y el momento de ejecutar

Qué pasa cuando un efecto escribe

La cascada: un efecto que escribe una fuente ensucia otros efectos dentro del mismo vaciado. Cuándo es legítimo, cuándo es un ciclo, y las tres alternativas a escribir desde un efecto.

⏱ 17 min

Un efecto que escribe una fuente convierte un ciclo de propagación en una cascada de ciclos. A veces es exactamente lo que hace falta y a veces es un bucle infinito disfrazado, y la línea entre ambos casos es más fina de lo que parece. Esta lección delimita cuándo la cascada converge, qué garantías se pierden por el camino, y las tres formas de evitarla que casi siempre son mejores.

🎯 Al terminar esta lección sabrás
  • Trazar una cascada de escrituras desde efectos paso a paso.
  • Determinar cuándo una cascada converge y cuándo no.
  • Reconocer los efectos que escriben lo que leen.
  • Aplicar las tres alternativas a escribir desde un efecto.

La cascada

Sigue este caso con el código del vaciado delante.

const paso = senal(0);
const validado = senal(false);

efecto(() => { if (paso() === 2) validado.set(comprobar()); });
efecto(() => { boton.disabled = !validado(); });

paso.set(2);

La escritura de paso marca el primer efecto y lo encola. Se vacía la cola: el primer efecto se ejecuta y escribe validado. Esa escritura ocurre durante el vaciado, así que el lote sigue abierto y el segundo efecto se añade a la misma cola. El bucle del vaciado, al usar un conjunto recorrido con for...of, recoge el efecto añadido y lo ejecuta.

Resultado: dos rondas de efectos dentro de un solo vaciado. Es correcto y es lo que se quiere.

flowchart TB
W[escritura en paso] --> V[vaciado del lote]
V --> E1[efecto uno se ejecuta]
E1 --> S[escribe en validado]
S --> Q[efecto dos entra en la misma cola]
Q --> E2[efecto dos se ejecuta]
E2 --> F[vaciado terminado]
style W fill:#89b4fa,color:#11111b
style V fill:#cba6f7,color:#11111b
style E1 fill:#a6e3a1,color:#11111b
style S fill:#f9e2af,color:#11111b
style Q fill:#f9e2af,color:#11111b
style E2 fill:#a6e3a1,color:#11111b
style F fill:#a6e3a1,color:#11111b

Cuándo converge

La cascada termina si y solo si el sistema alcanza un punto fijo: una ronda en la que ningún efecto escribe nada que no estuviera ya escrito.

Hay tres condiciones que garantizan la convergencia, y basta con incumplir una para tener un bucle.

El grafo de escrituras entre efectos es acíclico. Si el efecto A escribe una fuente que el efecto B lee, y B no escribe nada que A lea, la cascada es una cadena que termina.

La escritura es idempotente. Escribir el mismo valor dos veces no propaga la segunda, porque la comparación de igualdad lo corta. Esto salva muchas cascadas que técnicamente son cíclicas.

La transformación converge. Si A escribe en B una función de B, esa función tiene que tener un punto fijo alcanzable. b.set(x => x) converge de inmediato; b.set(x => x + 1) no converge nunca.

// Converge: la igualdad corta en la segunda vuelta
efecto(() => { normalizado.set(bruto().trim()); });

// NO converge: cada vuelta produce un valor distinto
efecto(() => { contador.set(contador() + 1); });

// Trampa clasica: dos efectos que se sincronizan y no son idempotentes
efecto(() => { celsius.set(aCelsius(fahrenheit())); });
efecto(() => { fahrenheit.set(aFahrenheit(celsius())); });
// si la conversion de ida y vuelta redondea, oscila para siempre

El tercer caso es el que más se escribe sin darse cuenta. Dos efectos que mantienen sincronizadas dos representaciones del mismo dato convergen solo si la conversión de ida y vuelta es exactamente la identidad. Con redondeos, formateo de cadenas o normalización, no lo es, y el resultado es una oscilación que solo detiene la protección del contador de iteraciones.

🛑
La proteccion del contador oculta el bug en lugar de resolverlo

Cuando saltan las diez mil iteraciones y el motor lanza su error de bucle infinito, la reacción habitual es buscar el efecto que sale en la traza. Casi nunca es el culpable: la traza señala el último de la cadena, no el par de efectos que se están empujando. El diagnóstico correcto es listar todos los efectos que escriben fuentes, construir el grafo de escrituras entre ellos, y buscar el ciclo. Es un grafo pequeño y el ciclo salta a la vista.

Qué se pierde en una cascada

Aunque converja, una cascada tiene costes que conviene conocer.

Los efectos intermedios son observables. En el ejemplo de arriba, entre la primera y la segunda ronda el botón todavía tiene el estado viejo. Si un efecto de la primera ronda escribe en el DOM y otro de la segunda lo corrige, hay un estado intermedio pintado.

El orden se vuelve difícil de razonar. Con tres rondas y cinco efectos, deducir qué corre cuándo exige simular el vaciado mentalmente. Es exactamente el tipo de código que nadie quiere depurar.

El coste se multiplica. Cada ronda es un ciclo completo de marcado y evaluación. Una cascada de cuatro rondas cuesta cuatro veces lo que una propagación normal.

Alternativas y cierre del nivel

Las tres alternativas

Casi siempre que un efecto escribe una fuente, hay una formulación mejor. Tres, en concreto.

Alternativa uno: conviértelo en una derivación. Si el valor que escribes es función de lo que lees, no es estado: es estado derivado, y debería ser un memo.

// En vez de esto
efecto(() => { total.set(elementos().reduce(sumar, 0)); });

// Esto
const total = memo(() => elementos().reduce(sumar, 0));

Es la sustitución más frecuente y la que más mejora el sistema: elimina una fuente, elimina un efecto, elimina una ronda de cascada, y hace el valor perezoso y cacheado.

Alternativa dos: escribe las dos en el mismo lote. Si dos señales tienen que cambiar juntas, escríbelas juntas en la función que provoca el cambio, en vez de que una desencadene la otra.

// En vez de que un efecto reaccione
function seleccionarPagina(n) {
  lote(() => { pagina.set(n); seleccion.set([]); });
}

Alternativa tres: una fuente escribible con valor derivado por defecto. El caso legítimo que queda es el estado que normalmente se deriva pero que el usuario puede sobrescribir, y que se resetea cuando cambia la fuente. Angular tiene una primitiva específica para esto, linkedSignal, que es una señal escribible que se recalcula cuando su fuente cambia. En otros motores se construye a mano con una señal de versión.

// Patron manual: una senal escribible que se resetea al cambiar la fuente
function senalEnlazada(fuente, calcular) {
  const local = senal(calcular(fuente()));
  efecto(() => { const v = fuente(); local.set(calcular(v)); });
  return local;
}

Aquí el efecto que escribe es intencionado y la cascada es de una sola ronda, porque el efecto no lee la señal que escribe. Es el uso legítimo.

Un efecto que escribe es casi siempre una derivacion mal colocada

La regla de diagnóstico que resume el nivel es esta, y aguanta sorprendentemente bien: cuando encuentres un efecto que escribe una fuente, pregúntate si el valor que escribe es función de lo que lee. Si lo es —y lo es en la gran mayoría de los casos— no es estado, es estado derivado, y ponerlo en una fuente es duplicar información que ya está en el grafo. Esa duplicación es la causa raíz de casi todos los problemas de esta lección: la cascada existe porque hay dos copias del mismo hecho y hay que sincronizarlas; el ciclo existe porque las dos copias se sincronizan mutuamente; el estado intermedio observable existe porque durante un instante las dos copias discrepan. Convertirlo en una derivación elimina las tres cosas de golpe, porque elimina la copia. Quedan dos casos legítimos y conviene saber reconocerlos: el estado que es derivado pero sobrescribible por el usuario, que es lo que resuelve una señal enlazada; y la escritura hacia un sistema externo que resulta ser reactivo, como sincronizar con la URL o con el almacenamiento, donde la duplicación es inevitable porque una de las copias no la controlas tú. Fuera de esos dos, un efecto que escribe es una señal de que el grafo se ha modelado mal, y el arreglo casi nunca es añadir un untrack o un lote para que deje de dar problemas: es mover el valor al sitio donde le corresponde.

Cierre del nivel

El lote agrupa escrituras y devuelve la atomicidad que la granularidad fina había quitado. La cola separa el marcado de la ejecución y es parte de la garantía de consistencia. La política síncrona o asíncrona decide quién lleva el reloj. La separación entre puras y efectos marca la frontera entre lo reversible y lo irreversible. Y un efecto que escribe es casi siempre una derivación mal colocada.

Lo que queda es la última gran decisión de diseño: si el estado se modela átomo a átomo o como objetos profundos instrumentados con proxies. Es el nivel 9.

⚔️ Detecta ciclos entre efectos
  1. Escribe una función que recorra tus efectos y registre qué fuentes lee y cuáles escribe.
  2. Construye el grafo dirigido de escrituras entre efectos y busca ciclos.
  3. Escribe el par de efectos de conversión con redondeo y comprueba que oscila.
  4. Sustituye uno de los dos por una derivación y verifica que la oscilación desaparece.