wandres.dev
CREATEEFFECT · reacciones a los signals

Tracking automático de dependencias

Cómo un efecto descubre solo de qué depende: leer un signal dentro del scope reactivo crea la suscripción. El mecanismo del listener, las dependencias que cambian en cada run y las lecturas que no trackean.

⏱ 13 min

La pregunta que define a un sistema reactivo es: ¿cómo sabe un efecto de quién depende? React responde con una lista escrita a mano, con todos los bugs de olvidar o de sobrar un elemento. Solid responde con una sola regla, casi mágica en su simplicidad: si lees un signal mientras el efecto corre, quedas suscrito a él. Nada más. Ni listas, ni declaraciones. El efecto descubre sus dependencias por el mero acto de leerlas, y las vuelve a descubrir en cada ejecución.

🎯 Al terminar esta lección sabrás
  • Entender que leer un signal dentro de un efecto crea la suscripción, y solo eso.
  • Ver el mecanismo del listener: quién está escuchando durante la ejecución.
  • Comprender las dependencias dinámicas: cambian en cada run según el camino tomado.
  • Saber qué lecturas NO trackean: fuera del scope, en callbacks async o bajo untrack.

Leer es suscribirse

Un signal en Solid es una función. No es azar sintáctico: la llamada n() es el evento observable. Cuando un efecto está corriendo, Solid mantiene una referencia global al listener actual —la computación en curso—.

Cada vez que se invoca el getter de un signal, ese getter mira quién es el listener y, si hay uno, teje un enlace bidireccional: el signal apunta al efecto como observador, y el efecto apunta al signal como fuente.

createEffect(() => {
  // mientras corre este cuerpo, el listener global = este efecto
  console.log(a(), b()); // a() y b() registran la suscripcion al leerse
});

Ese enlace se teje en tiempo de ejecución, en el instante de la lectura. No hay análisis estático, no hay compilador mirando qué nombres aparecen: es puramente dinámico. La consecuencia es exacta: el efecto depende de aquello que de hecho leyó, ni más ni menos.

El mecanismo se propaga a través de los memos sin que hagas nada. Si un efecto lee un memo y el memo lee dos signals, el efecto queda atado a esos dos signals de forma transitiva: cuando cualquiera cambie, el memo se recomputa y, si su valor cambió, el efecto reacciona.

const [precio, setPrecio] = createSignal(100);
const [iva, setIva] = createSignal(0.21);
const total = createMemo(() => precio() * (1 + iva()));

createEffect(() => console.log("total", total())); // reacciona a precio e iva

Dependencias dinámicas

Antes de cada ejecución, Solid borra las suscripciones anteriores del efecto y las vuelve a recolectar desde cero durante el nuevo run. Las dependencias no son fijas: cambian de una ejecución a otra según qué ramas se tomen.

const [mostrar, setMostrar] = createSignal(true);
const [detalle, setDetalle] = createSignal("hola");

createEffect(() => {
  if (mostrar()) {
    console.log(detalle());
  } else {
    console.log("oculto");
  }
});

Cuando mostrar es true, el efecto lee detalle y queda suscrito a ambos. En el momento en que mostrar pasa a false, el efecto vuelve a correr, ya no entra en la primera rama, no lee detalle, y se desuscribe de él.

A partir de ahí, cambiar detalle no dispara nada hasta que mostrar vuelva a ser true. Las dependencias siguen al flujo de control real, no al texto del programa. Esto es imposible de expresar con una lista estática, y es una de las razones por las que el tracking automático borra toda una familia de bugs.

📝
Por eso los signals se llaman como funciones

La forma n() en lugar de n no es verbosidad: es el gancho de la suscripción. Si Solid expusiera el valor como una propiedad plana, no habría ningún punto donde interceptar la lectura y registrar la dependencia. Al ser una llamada, cada acceso pasa por un getter que consulta el listener actual. La sintaxis que a veces incomoda a quien llega de React es, precisamente, el mecanismo que hace posible el tracking sin declarar nada.

Lecturas que no trackean

La regla tiene un reverso igual de importante: solo trackean las lecturas que ocurren de forma síncrona dentro del cuerpo del efecto, mientras el listener está activo. Hay tres situaciones donde lees un signal y aun así no te suscribes.

La primera: leer fuera de todo scope reactivo. Un n() en el nivel superior de un módulo, sin efecto ni memo alrededor, devuelve el valor pero no suscribe a nadie, porque no hay listener.

La segunda: leer dentro de un callback asíncrono. Cuando el efecto arranca un setTimeout o un then de promesa, ese callback corre más tarde, cuando el efecto ya terminó y el listener ya no existe.

createEffect(() => {
  const id = usuarioId();   // SI trackea: lectura sincrona
  fetch(`/api/${id}`).then(() => {
    console.log(pagina());  // NO trackea: el listener ya se fue
  });
});

La tercera: leer bajo untrack. A veces quieres el valor de un signal sin depender de él —consultarlo sin que sus cambios te reactiven—. untrack(fn) ejecuta fn con el listener temporalmente apagado, así que las lecturas de dentro no suscriben.

import { untrack } from "solid-js";

createEffect(() => {
  const q = consulta();                 // dependencia real
  const t = untrack(() => tema());      // se lee, pero no reactiva
  registrar(q, t);
});

Aquí el efecto reacciona a consulta pero no a tema: cuando cambie el tema, el efecto no correrá, aunque su último valor sí se usó. Es la herramienta para decir “quiero el dato, no la dependencia”.

⚠️
El desliz clásico: esperar reactividad dentro de un async

Si dependes de un signal que solo lees dentro de un .then o un await, tu efecto no reaccionará a él y arrastrarás valores viejos. La cura es leer los signals que te importan arriba, de forma síncrona, y pasar esos valores capturados al código asíncrono. Lo tratamos a fondo en la lección de errores comunes, pero interiorízalo ya: el tracking termina cuando el cuerpo síncrono termina.

El tracking automático es una inversión de responsabilidad

Detente en lo que Solid ha hecho aquí, porque es el corazón de su ergonomía. En el modelo de React, tú eres responsable de declarar las dependencias, y el framework confía en tu palabra: si tu lista miente —por olvido, por un cierre obsoleto, por una refactorización a medias—, obtienes un efecto que no reacciona cuando debía o que reacciona con datos rancios, y el compilador no puede ayudarte porque para él la lista es solo un array. Solid invierte la carga: nadie declara nada, el sistema observa qué leíste realmente y construye el grafo a partir de ese hecho, no de una promesa. La lista de dependencias no puede estar equivocada porque no existe: las dependencias son las lecturas. Esto no solo borra una clase entera de bugs —los de dependencias omitidas o sobrantes—, sino que además las hace dinámicas y precisas hasta un grano imposible de mantener a mano: si una rama no se ejecuta, sus signals no cuentan; si vuelve a ejecutarse, vuelven a contar, sin que escribas una línea. El precio es una sola disciplina, y es pequeña: lee tus dependencias de forma síncrona, dentro del scope. A cambio, el grafo siempre dice la verdad sobre quién depende de quién, porque lo aprendió mirándote leer.

⚔️ Descubre tus propias dependencias
  1. Escribe un efecto con una rama if y comprueba, cambiando el signal de la condición, cómo aparece y desaparece la dependencia de la rama no tomada.
  2. Interpón un createMemo entre un signal y un efecto y confirma que el efecto reacciona al signal de forma transitiva.
  3. Lee un signal dentro de un setTimeout disparado desde un efecto y verifica que cambiarlo después no re-ejecuta el efecto.
  4. Envuelve una lectura en untrack y confirma que el valor se usa pero el efecto ya no reacciona a ese signal.