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.
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 contextocreateEffect(() => { const q = consulta(); // gatillo const f = filtros(); // gatillo const pag = untrack(() => pagina()); // solo lectura buscar(q, f, pag);});// Sumar: casi nada dispara, salvo una fuentecreateEffect( 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.
El 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é.