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.
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, simplificadofunction 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.
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);});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 itemsconst 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.