wandres.dev
CREATEEFFECT · reacciones a los signals

Errores comunes con efectos

Los tres tropiezos del efecto: escribir un signal que el propio efecto lee y provocar un bucle, usar efectos para derivar estado en vez de un memo, y sincronizar con el mundo externo sin fugas ni lecturas rancias. untrack y on como herramientas.

⏱ 15 min

Con lo aprendido —qué es un efecto, cómo trackea, cómo limpia y cuándo corre— ya puedes escribir efectos correctos. Falta lo que separa a quien los usa de quien los domina: reconocer los tres modos en que un efecto se tuerce. Casi todos los bugs de efectos en Solid son variantes de una misma confusión: usar un efecto para algo que pertenece dentro del grafo, o olvidar que un efecto toca el mundo de fuera. Verlos con nombre y apellido es vacunarse contra ellos.

🎯 Al terminar esta lección sabrás
  • Reconocer y romper el bucle: un efecto que escribe un signal que él mismo lee.
  • Dejar de derivar estado con efectos y usar createMemo en su lugar.
  • Sincronizar con el mundo externo sin fugas, con onCleanup y lecturas síncronas.
  • Controlar el tracking con untrack y con on cuando de verdad haga falta.

El bucle: escribir lo que lees

Si un efecto lee un signal y también lo escribe, se dispara a sí mismo: la escritura ensucia una dependencia de la que el propio efecto depende, y el ciclo se realimenta. Solid vigila las actualizaciones desbocadas y llega a lanzar un error de bucle potencial, pero el error es el síntoma; la enfermedad es haber cerrado el lazo.

// BUG: el efecto lee y escribe `total`, se realimenta sin fin
createEffect(() => {
  setTotal(total() + item());
});

Hay tres salidas, en orden de preferencia inversa. La más tosca: untrack la lectura, para leer total sin depender de él y cortar el lazo.

import { untrack } from "solid-js";

createEffect(() => {
  const i = item();
  setTotal(untrack(total) + i); // lee total sin suscribirse
});

Funciona, pero delata el verdadero problema: eso no era una reacción, era una derivación. total es función de sus sumandos; expresarlo como un efecto que se muerde la cola es forzar con una herramienta lo que otra hace de forma natural.

No derives estado con efectos

El anti-patrón más extendido es usar un efecto para copiar un signal en otro: leer A, calcular algo y hacer setB. Cada vez que veas un efecto cuyo único trabajo es un setX, casi siempre querías un createMemo.

// anti-patron: espejar un signal en otro con un efecto
const [nombre, setNombre] = createSignal("Ada");
const [saludo, setSaludo] = createSignal("");
createEffect(() => setSaludo(`Hola ${nombre()}`));

// idiomatico: una derivacion pura, sin estado duplicado
const saludo = createMemo(() => `Hola ${nombre()}`);

La versión con efecto tiene tres defectos que la de memo no tiene. Introduce un tick de retraso: saludo va un paso por detrás de nombre hasta que corre el efecto. Duplica el estado en dos signals que pueden desincronizarse. Y añade un nodo de escritura al grafo donde bastaba uno de lectura.

El memo, en cambio, es pull: se recalcula solo cuando alguien lo lee y su valor cambió, siempre coherente, sin estado espejo. La regla es nítida: si ambos lados de la sincronización viven dentro de Solid, no quieres un efecto, quieres una derivación.

📝
La misma lección que React aprendió tarde

La comunidad de React acabó escribiendo guías enteras sobre “quizá no necesitas un efecto”, precisamente contra este hábito de sincronizar estado con estado. En Solid el mensaje es idéntico pero el modelo de grano fino lo hace más afilado: como derivar es tan barato y directo con createMemo, casi cualquier efecto que termina en un setX es una derivación disfrazada. Reserva los efectos para cuando uno de los dos lados no es reactivo.

Sincronizar con el mundo externo, con cuidado

Cuando un lado no es reactivo —el DOM, la red, una suscripción, el localStorage, un log— el efecto sí es la herramienta correcta: es el puente entre el grafo y lo impuro. Persistir un signal en el almacenamiento del navegador es el ejemplo canónico de uso legítimo: uno de los lados, el localStorage, está fuera de Solid.

// uso correcto: sincronizar estado reactivo con un mundo NO reactivo
createEffect(() => {
  localStorage.setItem("tema", tema()); // el destino no es un signal
});

Ese efecto no deriva nada ni se muerde la cola: proyecta un valor reactivo hacia fuera. Pero el puente pide tres cautelas.

Primera: limpia siempre. Toda suscripción, temporizador o petición en vuelo debe cancelarse en onCleanup, o acumularás listeners zombis y respuestas que llegan tarde y pisan el estado.

Segunda: lee tus dependencias arriba, de forma síncrona; si lees un signal dentro de un .then o tras un await, ni trackeas ni ves el valor vigente.

Tercera: cuando quieras dependencias explícitas y opcionalmente saltarte el primer run, usa on.

import { createEffect, on, onCleanup } from "solid-js";

createEffect(on(consulta, (q) => {
  const ctrl = new AbortController();
  buscar(q, ctrl.signal).then(setResultados); // q capturado, sincrono
  onCleanup(() => ctrl.abort());              // cancela la peticion vieja
}));

on(consulta, fn) declara que este efecto reacciona solo a consulta, aunque dentro leas otros signals: lo demás no lo re-dispara. Y con la opción defer el efecto no corre en el montaje, solo cuando consulta cambia por primera vez de verdad.

createEffect(on(consulta, (q) => buscar(q), { defer: true }));
// no busca al montar; espera al primer cambio real de `consulta`

Es el escape controlado del tracking automático para los casos en que quieres mandar tú.

⚠️
La petición que llega tarde

Sin el onCleanup que aborta, este patrón esconde una condición de carrera: si consulta cambia rápido, varias peticiones quedan en vuelo y la que responda última —no la más reciente— será la que fije setResultados. Abortar la anterior en la limpieza no es higiene opcional: es lo que garantiza que el resultado mostrado corresponde a la consulta actual. Todo efecto que dispara trabajo asíncrono debería poder cancelarlo.

Los efectos son el borde impuro; trátalos como tal

Reúne los tres errores y verás que son el mismo error mirado desde tres ángulos. El bucle, la derivación disfrazada y la fuga de recursos nacen todos de no respetar la frontera que un efecto representa. Un efecto es, por definición, el punto donde la reactividad pura se encuentra con lo que no es reactivo: el DOM, la red, el reloj, el disco. Dentro de esa frontera, todo debería resolverse con signals y memos —valores que se derivan unos de otros de forma declarativa y perezosa—; cruzarla debería ser raro, explícito y siempre reversible. El bucle ocurre cuando metes dentro del efecto una relación que era pura derivación, y el grafo, que no distingue tu intención, la realimenta. La derivación con efectos ocurre cuando desconfías del pull de los memos e insistes en empujar el estado a mano, pagando un tick y un signal de más. La fuga ocurre cuando cruzas la frontera para abrir un recurso pero no dejas el camino de vuelta para cerrarlo. La disciplina que los cura a los tres es una sola y cabe en una frase: deriva con memos, reacciona con efectos, y trata cada efecto como dueño de los recursos externos que debe soltar. Cuando un efecto solo lee y escribe signals, sospecha: probablemente describías una derivación. Cuando un efecto toca el mundo, pregúntate qué reservó y dónde lo suelta. Los efectos son pocos, deliberados y limpios; si los tuyos se multiplican, se muerden la cola o dejan cabos, casi siempre es que algo que pertenecía al grafo se escapó al borde.

⚔️ Caza tus propios anti-patrones
  1. Provoca a propósito un bucle con un efecto que lea y escriba el mismo signal, observa el aviso de Solid y córtalo con untrack.
  2. Reescribe ese mismo caso como createMemo y argumenta por qué ya no hay bucle posible.
  3. Toma un par de signals sincronizados por un efecto (setB desde A) y sustitúyelos por un único memo; comprueba que desaparece el tick de retraso.
  4. Escribe un efecto de búsqueda con on, defer y AbortController, dispara varias consultas seguidas y verifica que solo gana la última.