Combinar untrack y on: control fino
untrack y on son las dos caras de una misma decisión: qué fuente dispara una reacción y cuál solo aporta contexto. Cómo elegir entre carvar una excepción con untrack en un cuerpo que rastrea, o invertir el modelo con on cuando casi nada debe disparar. Patrones para repartir cada signal entre el conjunto gatillo y el conjunto lectura.
untrack y on parecen herramientas distintas, pero resuelven la misma pregunta desde extremos opuestos: ¿qué fuentes deben disparar esta reacción y cuáles solo informarla? El rastreo automático no distingue esos dos papeles —toda lectura rastreada pesa igual—, y estas dos primitivas son cómo los separas. La maestría en Solid no está en conocerlas por separado, sino en saber cuál usar según cuántas fuentes gatillan: pocas excepciones se carvan con untrack; muchas excepciones se invierten con on.
- Ver
untrackyoncomo respuestas complementarias al mismo reparto de papeles. - Elegir
untrackcuando casi todo dispara salvo una o dos lecturas de contexto. - Elegir
oncuando casi nada dispara salvo una o dos fuentes gatillo. - Diseñar reacciones repartiendo cada signal entre conjunto gatillo y conjunto lectura.
Dos formas de trazar la misma frontera
Toda reacción divide sus fuentes en dos conjuntos: el conjunto gatillo, las que provocan que corra, y el conjunto lectura, las que consulta para producir su resultado pero que no deben despertarla. El rastreo automático mete todo lo que lees en el conjunto gatillo. untrack y on existen para sacar cosas de ahí, y difieren solo en la dirección del ajuste.
- Con
untrack, partes de “todo dispara” y restas excepciones: rodeas las lecturas de contexto para expulsarlas del conjunto gatillo. - Con
on, partes de “nada dispara” y sumas los gatillos: enumeras en la lista de deps las pocas fuentes que sí deben provocar la reacción.
// Restar: casi todo dispara, salvo una lectura de contexto
createEffect(() => {
const q = consulta(); // gatillo
const f = filtros(); // gatillo
const pag = untrack(() => pagina()); // solo lectura
buscar(q, f, pag);
});
// Sumar: casi nada dispara, salvo una fuente
createEffect(
on(consulta, () => {
buscar(consulta(), filtros(), pagina()); // filtros y pagina solo se leen
})
);
Ambos fragmentos son legítimos, pero expresan intenciones distintas. El primero dice “reacciono a la consulta y a los filtros, pero la página es contexto”. El segundo dice “solo la consulta manda; lo demás lo tomo como esté”. La forma que elijas es una afirmación sobre el dominio, no un detalle de estilo.
La regla de la mayoría
El criterio para elegir es cuantitativo y sencillo: usa la primitiva que te obligue a escribir menos excepciones. Si el conjunto gatillo es casi todo y solo excluyes una o dos fuentes, untrack sobre esas es lo más limpio. Si el conjunto gatillo es diminuto y excluyes casi todo, on con esas pocas deps gana.
flowchart TD
P[diseno la reaccion] --> Q{cuantas fuentes disparan}
Q -->|casi todas salvo una| UT[cuerpo normal con untrack en la excepcion]
Q -->|casi ninguna salvo pocas| ON[usa on con esas deps]
UT --> L[el resto se lee sin suscribir]
ON --> L
style UT fill:#89b4fa,color:#11111b
style ON fill:#cba6f7,color:#11111b
style L fill:#a6e3a1,color:#11111bEl anti-patrón que esta regla evita es el de las excepciones amontonadas: un on con ocho deps es una lista frágil que olvidarás actualizar, y un cuerpo con seis untrack es un colador donde ya nadie sabe qué dispara. Cuando cuentes más de dos o tres excepciones en cualquiera de las dos direcciones, es señal de que elegiste la primitiva equivocada; date la vuelta y usa la otra.
Combinarlas de verdad: on como marco, untrack como matiz
Nada te obliga a elegir solo una. Como el cuerpo de on ya corre sin rastrear, dentro de él todas las lecturas son inertes por defecto, así que rara vez necesitas untrack allí. Pero hay un patrón potente en la dirección inversa: usar on para fijar el disparador principal y, si dentro debes reactivar el rastreo de algo puntual, envolver ese fragmento en un scope reactivo anidado.
El caso más habitual y honesto de combinación aparece cuando on gobierna el disparo pero el cuerpo debe leer un valor previo propio y decidir con él, mezclando el prevInput que on ofrece con lecturas de contexto:
createEffect(
on(rutaActual, (ruta, rutaPrevia) => {
// ruta y rutaPrevia vienen de on; el resto es contexto inerte
const scroll = posicionGuardada(); // lectura, no dispara
if (ruta !== rutaPrevia) {
registrarNavegacion(ruta, rutaPrevia, scroll);
}
})
);
Aquí on aporta la disciplina del disparador —solo la ruta manda— y el prevInput te da la comparación con el estado anterior sin un signal de memoria aparte. Es la síntesis de las dos lecciones previas: el disparo explícito de on y la lectura inerte que en un efecto normal habrías tenido que forzar con untrack.
Un caso completo: autoguardado
Reúne todo en un componente que autoguarda un borrador. La regla del dominio es nítida: guardar cuando cambie el contenido, tomando el usuario y las preferencias como contexto, y sin guardar en el montaje.
function Editor() {
const [contenido, setContenido] = createSignal("");
const [usuario] = useUsuario();
const [prefs] = usePreferencias();
// dispara: contenido | contexto: usuario, prefs | no guarda al montar
createEffect(
on(
contenido,
(texto, textoPrevio) => {
if (texto === textoPrevio) return;
const autor = usuario().id; // contexto inerte
const formato = prefs().formato; // contexto inerte
guardarBorrador(texto, autor, formato);
},
{ defer: true }
)
);
return <textarea onInput={(e) => setContenido(e.currentTarget.value)} />;
}
Analiza el reparto de papeles pieza por pieza. on(contenido, ...) fija el único disparador: solo teclear cambia el borrador. Las lecturas de usuario() y prefs() son contexto —se consultan en el momento de guardar sin volverse gatillos—, y como el cuerpo de on no rastrea, ni siquiera hace falta un untrack para blindarlas: el marco ya las deja inertes. defer: true evita un guardado espurio con el contenido vacío inicial. Y prevInput aporta la comparación que descarta guardados redundantes sin un signal de memoria aparte. Cuatro decisiones de causalidad, cada una expresada por la pieza exacta, sin una sola guarda manual improvisada.
Reescribe mentalmente ese efecto con el modelo por defecto y verás el contraste. Un createEffect que lee contenido(), usuario() y prefs() se dispararía también al cambiar el usuario o las preferencias —guardando sin que el texto se haya tocado— y correría en el montaje con el borrador vacío. Para domarlo necesitarías dos untrack y una guarda de primera vez incrustada en el cuerpo. on con defer expresa esas tres correcciones de golpe y las deja a la vista en la firma, no enterradas en condicionales. Ese es el rédito de repartir papeles: menos código, y cada línea diciendo la verdad sobre qué causa qué.
Antes de teclear la reacción, escribe en un comentario dos listas: dispara y solo lee. Ese acto —clasificar cada fuente— es el diseño de verdad; la elección entre untrack y on cae sola después. Si la lista dispara es larga y la de solo lee corta, será untrack; si es al revés, será on. Programadores que saltan directo al código sin este reparto acaban con reacciones que se disparan de más o de menos, y depurar eso cuesta diez veces lo que cuesta la clasificación previa.
Si te descubres poniendo on con tres deps y dos untrack dentro y una guarda manual, detente: no estás afinando, estás tapando que no tienes claro qué gobierna la reacción. La combinación de primitivas es para expresar un reparto de papeles nítido, no para parchear uno borroso. Cuando la frontera entre gatillo y lectura no está clara en tu cabeza, ninguna combinación de untrack y on la aclarará en el código; primero decide el reparto, luego elige la herramienta.
El salto mental que separa a quien usa Solid de quien lo domina es dejar de pensar una reacción como “código que lee unos signals y hace algo” y empezar a pensarla como una declaración de causalidad: qué hechos del mundo causan que esto vuelva a ocurrir. En esa lectura, untrack y on no son dos APIs que memorizar, sino las dos operaciones primitivas del álgebra de la causalidad reactiva —restar una causa y fijar el conjunto de causas—. El rastreo automático toma una decisión por defecto perfectamente razonable: toda fuente que lees es una causa. Es correcta el noventa por ciento del tiempo, y por eso es el modo por defecto. Pero el diez por ciento restante es donde vive la lógica interesante: el dato que consultas para computar pero cuya evolución tiene su propio canal, la operación que debe recomputarse por un eje y no por los otros que también toca, el watcher que reacciona al cambio deliberado e ignora el estado inicial. En todos esos casos, el reparto por defecto es demasiado grueso, y afinarlo no es una optimización: es corregir qué significa tu reacción. Cuando repartes cada signal entre “dispara” y “solo informa” estás modelando el dominio con una precisión que el rastreo automático, por bueno que sea, no puede alcanzar solo, porque no sabe cuál de tus lecturas es una causa y cuál un mero insumo —esa distinción vive en tu cabeza, no en el código—. untrack y on son cómo la sacas de tu cabeza y la escribes. Elegir entre restar y sumar es solo aritmética: minimiza las excepciones. Pero el acto de repartir, ese es el diseño, y es lo que convierte un puñado de efectos que “más o menos funcionan” en un grafo cuya causalidad puedes leer, razonar y defender línea por línea.
- Toma un efecto que se dispara de más y clasifica sus fuentes en dos listas: dispara y solo lee.
- Si la lista solo lee tiene una o dos fuentes, arréglalo envolviéndolas en
untracky confirma que ya no se dispara de más. - Reescribe ese mismo efecto con
on, poniendo en las deps solo las fuentes dispara; compara cuál de las dos versiones lee mejor. - Construye un efecto con
on(ruta, (ruta, previa) => ...)que compare el valor previo y lea una posición de scroll como contexto inerte. - Cuenta las excepciones de un efecto tuyo real: si tienes más de tres en una dirección, invierte la primitiva y verifica que la lista se acorta.