wandres.dev
UNTRACK, BATCH, ON · controlar el tracking

untrack: leer sin suscribirse

untrack ejecuta una función con el rastreo apagado: te entrega el valor actual de un signal sin tejer dependencia. Cuándo lo necesitas de verdad —leer estado auxiliar dentro de un efecto o memo sin re-ejecutarte, cortar bucles de realimentación— y por qué su abuso reintroduce justo los bugs de estado obsoleto que la reactividad de grano fino existía para erradicar.

⏱ 15 min

Toda la reactividad de Solid descansa en un pacto tácito: leer un signal dentro de un ámbito de rastreo te suscribe a él. untrack es la cláusula de excepción de ese pacto. Ejecuta una función y, durante ese instante, apaga el rastreo: cualquier getter que invoques te entrega su valor actual sin tejer la menor dependencia. Es una herramienta de cirujano —precisa, poderosa y peligrosa—, porque romper el pacto a la ligera reintroduce exactamente los bugs de estado obsoleto que la reactividad de grano fino existía para erradicar.

🎯 Al terminar esta lección sabrás
  • Entender qué hace untrack y por qué devuelve el valor sin crear suscripción.
  • Reconocer los usos legítimos: leer estado auxiliar dentro de un efecto o memo.
  • Identificar el peligro central: la lectura congelada que nunca se refresca.
  • Adoptar el criterio de cuándo untrack resuelve un problema y cuándo lo esconde.

La cláusula de excepción del pacto reactivo

untrack recibe una función, la ejecuta con el rastreo desactivado y devuelve lo que esa función retorne. Durante esa ejecución, cualquier signal que leas te da su valor del momento sin registrarte como observador. Su firma es tan sobria como su propósito: untrack<T>(fn: () => T): T.

import { createSignal, createEffect, untrack } from "solid-js";

const [a, setA] = createSignal(0);
const [b, setB] = createSignal(0);

createEffect(() => {
  const dep = a();                 // dependencia: vuelve a correr al cambiar a
  const foto = untrack(() => b()); // lectura inerte: b no crea dependencia
  console.log(dep, foto);
});

Este efecto se re-ejecuta cuando cambia a, jamás cuando cambia b. Has leído ambos signals con idéntica sintaxis, pero solo uno quedó cableado. untrack no oculta el valor de b: te da el más reciente en el instante de correr; lo que descarta es la suscripción, el hilo que conectaría un cambio futuro de b con una nueva ejecución.

Conviene fijar de entrada la naturaleza de esa lectura: es puntual. untrack(() => b()) es una fotografía tomada cuando el efecto corre, no una ventana viva. Si el efecto no vuelve a ejecutarse por otra causa, la foto no se refresca nunca. Toda la utilidad y todo el peligro de untrack brotan de esa única propiedad. Como el argumento es una función sin parámetros, puedes pasar el getter directamente: untrack(b) equivale a untrack(() => b()).

Cómo apaga el rastreo

Recupera el mecanismo del tracking automático: mientras una computación corre, Solid guarda una referencia global al listener actual, y cada getter que se invoca consulta esa referencia para tejer la suscripción. untrack opera justo sobre esa pieza —guarda el listener vigente, lo pone en null durante la ejecución de tu función y lo restaura al salir—.

// modelo mental de la implementación, simplificado
function untrack(fn) {
  const previo = Listener;   // guarda quién escuchaba
  Listener = null;           // nadie escucha durante fn
  try {
    return fn();             // los getters no hallan listener: no suscriben
  } finally {
    Listener = previo;       // restaura el estado anterior
  }
}

Verlo así disuelve tres confusiones frecuentes. Primero: untrack no “marca” el signal como no reactivo —el mismo signal sigue siendo rastreable en cualquier otra lectura de la aplicación—; solo silencia al oyente durante ese instante. Segundo: su alcance es dinámico y temporal, no léxico; si dentro de fn llamas a otra función que lee signals, esas lecturas tampoco rastrean, porque el listener sigue en null mientras dure la pila de llamadas. Y tercero, la simetría con la lección de lectura reactiva: leer un signal en un manejador de evento ya era inerte porque no había listener activo; untrack reproduce esa misma condición a voluntad dentro de un ámbito que sí lo tenía. No inventa un modo nuevo: recrea el vacío de rastreo que existe por defecto fuera de toda computación.

Los usos que lo justifican

untrack responde a una necesidad concreta y recurrente: quiero el valor actual de esto, pero no quiero que me despierte cuando cambie. Hay tres familias limpias de ese caso.

🎚️

Estado auxiliar que no gatilla

Un efecto reacciona a datos pero necesita el modo de presentación vigente. El modo condiciona el resultado, pero no debe provocar por sí solo una re-ejecución: se lee bajo untrack.

🔁

Cortar la realimentación

Un efecto que escribe en un signal y también lo lee para decidir el nuevo valor. Leerlo rastreado crearía un bucle; untrack lo consulta sin cerrarlo.

📊

Telemetría y logging

Registrar el estado completo en el momento de un evento sin suscribir el efecto a cada campo que solo aparece en el informe.

El patrón del disparador y el contexto es el más común. Separas las fuentes en dos papeles: las que provocan la reacción y las que solo aportan contexto en el momento de reaccionar.

createEffect(() => {
  const consulta = busqueda();              // dispara: reaccionamos al buscar
  const pag = untrack(() => pagina());      // contexto: no queremos disparar al paginar aqui
  fetchResultados(consulta, pag);
});
flowchart TD
E[el efecto corre su cuerpo] --> A[lee busqueda getter normal]
E --> B[lee pagina dentro de untrack]
A --> D[queda como dependencia]
B --> N[no queda como dependencia]
D --> R[cambia busqueda y vuelve a correr]
N --> S[cambia pagina y no pasa nada]
style D fill:#a6e3a1,color:#11111b
style N fill:#f38ba8,color:#11111b

Aquí el efecto se dispara cuando cambia busqueda, y en ese momento lee la pagina vigente sin suscribirse a ella. Si pagina tuviera su propio efecto de paginado, este otro no debe entrometerse. untrack reparte con precisión quién despierta a quién.

El peligro: la foto que se queda vieja

El reverso es severo. Cada untrack es una dependencia que decides no declarar, y si te equivocas al decidirlo, obtienes el bug más escurridizo de todos: algo que debería actualizarse y no lo hace, sin error, sin traza, sin nada que señale la causa.

// intención: recalcular el total cuando cambian los items
const total = createMemo(() => {
  const lista = items();               // dependencia correcta
  const tasa = untrack(() => iva());   // CONGELADO: si iva cambia, total NO se recomputa
  return lista.reduce((s, i) => s + i.precio * (1 + tasa), 0);
});

Si el iva sube, total seguirá calculado con la tasa vieja hasta que, por casualidad, cambien también los items y el memo vuelva a correr. Es un dato correcto por accidente y erróneo por diseño. El untrack de esa línea no aporta nada: solo amputa una dependencia que sí importaba.

Lo insidioso es que este bug no asoma en las pruebas obvias. Si tu test cambia los items, el memo recomputa y el total sale correcto; solo falla el escenario donde cambia únicamente el iva, que es justo el que nadie piensa en cubrir. Una dependencia amputada no rompe el caso feliz: sabotea un caso lateral que puede tardar meses en manifestarse en producción, cuando alguien ajuste una tasa y el número no se mueva. Por eso la regla es negativa y estricta: no saques del rastreo nada que no puedas jurar que es contexto.

⚠️
untrack no arregla bucles: los disimula

El uso más tentador y más peligroso es tapar un bucle infinito. Si un efecto se re-ejecuta sin fin porque escribe un signal que también lee, envolver la lectura en untrack corta el ciclo… y a menudo esconde que el diseño estaba mal planteado. Antes de recurrir a untrack, pregúntate si esa relación no era en realidad una derivación —un createMemo— o una reacción con dependencias explícitas —on, que verás en la lección 3—. untrack silencia el síntoma; esas primitivas curan la causa.

💡
Nómbralo para que se lea la intención

Un untrack anónimo obliga a quien lee a adivinar por qué esa lectura no debe rastrear. Extrae la intención a un nombre: const modoActual = untrack(modo) dice más que untrack(() => modo()) incrustado en una expresión. La reactividad de grano fino se apoya en que las dependencias sean legibles; una excepción invisible es deuda técnica que nadie verá hasta que falle.

Cada untrack es una promesa de que ese cambio no importa

La reactividad automática de Solid es un contrato de una sola cláusula: si lo lees mientras rastreo, te avisaré cuando cambie. Su enorme valor está en que no exige que recuerdes nada; el grafo se teje solo por el acto de leer, y por eso no existen listas de dependencias que olvidar ni completar de más. untrack es la única forma de romper ese contrato deliberadamente, y por eso debe leerse siempre como lo que es: una afirmación fuerte sobre el dominio del problema, no una comodidad sintáctica. Cuando escribes untrack(() => x()) estás jurando que un cambio futuro de x no debe cambiar este resultado. Si el juramento es verdadero —x es contexto, no causa; una tasa que ya se aplicó, una configuración estable durante esta operación, un valor cuya evolución tiene su propio canal— entonces untrack es exactamente la herramienta correcta y expresa esa verdad con precisión quirúrgica. Pero si el juramento es falso, has fabricado una lectura obsoleta con tus propias manos, y lo peor es que no fallará ruidosamente: devolverá un valor plausible, correcto la mayoría de las veces, equivocado justo cuando la fuente que amputaste cambie a solas. Ese es el bug que Solid diseñó su reactividad para hacer imposible, y untrack es la puerta por la que vuelve a entrar si abusas de ella. La disciplina, entonces, no es evitar untrack —a veces es imprescindible—, sino tratarlo como un acto de responsabilidad: cada uso es una excepción que debes poder justificar en una frase. Si no puedes nombrar por qué ese cambio no importa, es que sí importa, y lo que necesitas no es apagar el rastreo, sino declarar mejor tus dependencias.

⚔️ Amputa con criterio, no por reflejo
  1. Escribe un efecto que lea dos signals y envuelve uno en untrack; confirma en consola que solo el otro lo re-ejecuta.
  2. Reproduce el bug del total con iva bajo untrack: cambia el iva y observa que el total no se mueve hasta que tocas los items.
  3. Arregla ese memo quitando el untrack y razona por qué la dependencia sí era legítima.
  4. Construye el patrón disparador-contexto: un efecto que reacciona a busqueda y lee pagina con untrack; verifica que paginar no dispara el fetch.
  5. Toma un efecto con bucle infinito por escribir y leer el mismo signal, córtalo con untrack y luego reescríbelo con createMemo; decide cuál de los dos expresa mejor la intención.