Los cuatro límites del tracking automático
Asincronía, lecturas fuera de ámbito, escrituras que no pasan por la fuente y colecciones nativas: los cuatro puntos donde el descubrimiento de dependencias no funciona, con la razón mecánica de cada uno.
El tracking automático falla en cuatro sitios, siempre los mismos, en todos los motores. No son bugs ni carencias de implementación: son consecuencias directas del mecanismo que vimos en la primera lección del nivel. Conocerlos por su causa y no por su síntoma significa que puedes predecirlos en un motor que no has usado nunca, y que sabrás cuál es el remedio sin buscarlo.
- Deducir los cuatro límites a partir del mecanismo del observador actual.
- Distinguir el límite del síntoma en cada caso.
- Aplicar el remedio correcto a cada uno.
- Reconocer los mismos cuatro límites en motores distintos.
Límite 1: la asincronía
El observador actual se restaura al salir del marco síncrono. Toda lectura que ocurra después es invisible para el grafo.
efecto(async () => {
const id = usuarioId(); // rastreado
const datos = await pedirUsuario(id);
document.title = `${datos.nombre} - ${tema()}`; // tema NO se rastrea
});
El remedio no es un truco, es un cambio de forma: lee todo lo que necesites de forma síncrona al principio, y usa la parte asíncrona solo para el trabajo que no lee del grafo.
efecto(() => {
const id = usuarioId();
const t = tema(); // leidas las dos, sincronamente
(async () => {
const datos = await pedirUsuario(id);
document.title = `${datos.nombre} - ${t}`;
})();
});
Esto tiene una consecuencia de la que se habla poco: como el efecto puede reejecutarse mientras hay una petición en vuelo, la respuesta vieja puede llegar después de la nueva y sobrescribirla. Es una condición de carrera clásica, y la solución es la de siempre —cancelar al reejecutar—, que es exactamente para lo que sirve la limpieza del nivel 7.
Un valor asíncrono no encaja en el modelo de un valor síncrono con un getter, y por eso todos los motores acaban ofreciendo una primitiva específica: createResource en Solid, resource en Angular, o cualquier librería de estado de servidor sobre Vue. Todas hacen lo mismo por debajo: convierten una promesa en varias señales síncronas —datos, cargando, error— para que el grafo pueda observarlas con normalidad. La asincronía no se rastrea; se traduce a estado síncrono y ese sí se rastrea.
Límite 2: leer fuera de un ámbito reactivo
Si Observador es null, leer devuelve el valor y no teje nada. No hay error, no hay aviso en la mayoría de los casos.
const n = senal(0);
console.log(n()); // fuera de todo: devuelve 0 y no pasa nada
const doble = n() * 2; // captura el valor UNA vez, ya no es reactivo
La segunda línea es el error real: crea una constante, no una derivación. Es la misma trampa que la desestructuración del nivel 9, y en los dos casos la causa es idéntica: has leído el valor en lugar de conservar la manera de leerlo.
El remedio es entender la distinción entre el getter y el valor. Pasar n conserva la reactividad; pasar n() la pierde. Todos los motores tienen esta distinción y todos la expresan de forma distinta, lo que la hace confusa al cambiar de uno a otro: en Solid el getter es la propia función, en Vue es el objeto ref frente a .value, en Angular es la función señal frente a su llamada, y en Svelte el compilador la esconde del todo, lo que ayuda hasta que deja de ayudar.
Límite 3: mutar sin pasar por la fuente
El motor solo se entera de los cambios que pasan por su función de escritura. Cualquier mutación que esquive esa función es invisible.
const lista = senal([1, 2, 3]);
lista().push(4); // muta el array. El motor no se entera de nada.
lista.set([...lista(), 4]); // esto si notifica
En un motor de señales atómicas, el remedio es la disciplina de reemplazar en vez de mutar. En uno con estado profundo por proxies, el proxy intercepta la mutación y sí se entera, que es precisamente su razón de ser y el tema del nivel 9.
Hay un caso intermedio que confunde a mucha gente: mutar el interior de un objeto guardado en una señal atómica. La señal no cambia —es el mismo objeto—, así que la comparación de igualdad dice que no hay cambio y no se propaga nada, aunque el contenido sea distinto. Es correcto según el contrato del motor y sorprendente según la intuición.
El cuarto límite y el cierre
Límite 4: las colecciones nativas
Map, Set, Date y las clases nativas no son observables por proxy sin trabajo extra, porque sus métodos operan sobre ranuras internas que el proxy no puede interceptar de forma genérica. Un map.get(k) no pasa por ninguna trampa de propiedad.
Por eso los motores que ofrecen estado profundo envuelven estas clases explícitamente. Vue lo hace de forma transparente para Map y Set dentro de reactive. Svelte publica versiones reactivas en svelte/reactivity, con SvelteMap, SvelteSet, SvelteDate y SvelteURL. En un motor de señales atómicas, la forma habitual es guardar la colección en una señal y reemplazarla al modificarla, o mantener una señal auxiliar como contador de versión.
// Una version reactiva minima de Map sobre una senal de version
function mapaReactivo() {
const datos = new Map();
const version = senal(0);
return {
get(k) { version(); return datos.get(k); },
set(k, v) { datos.set(k, v); version.set(n => n + 1); },
get size() { version(); return datos.size; },
};
}
Es de grano grueso a propósito: cualquier escritura invalida a todos los lectores. Una implementación fina llevaría una señal por clave, y esa es exactamente la decisión que se estudia en el nivel 9.
flowchart TB L[limites del tracking] --> A[asincronia] L --> B[lectura fuera de ambito] L --> C[mutacion que esquiva la fuente] L --> D[colecciones nativas] A --> A1[leer sincronamente al principio] B --> B1[pasar el getter y no el valor] C --> C1[reemplazar o usar proxy] D --> D1[envolver la coleccion] style L fill:#f38ba8,color:#11111b style A fill:#f9e2af,color:#11111b style B fill:#f9e2af,color:#11111b style C fill:#f9e2af,color:#11111b style D fill:#f9e2af,color:#11111b style A1 fill:#a6e3a1,color:#11111b style B1 fill:#a6e3a1,color:#11111b style C1 fill:#a6e3a1,color:#11111b style D1 fill:#a6e3a1,color:#11111b
Míralos juntos y verás que son cuatro caras de una sola frase: el motor solo conoce lo que pasa por sus funciones, dentro de su ámbito síncrono. La asincronía sale del ámbito síncrono. La lectura fuera de ámbito no tiene ámbito. La mutación directa no pasa por su función de escritura. Las colecciones nativas no pasan por su función de lectura. Un único principio, cuatro manifestaciones. Y de ese principio sale la única regla de diagnóstico que necesitas cuando algo no se actualiza y no sabes por qué: pregúntate si la lectura ocurrió dentro del ámbito y si la escritura pasó por la fuente. En diez años de motores reactivos, y en cualquiera de los cinco que compararemos en el nivel 10, no vas a encontrar un fallo de este tipo que no sea una de las dos cosas. Es además una regla que se puede automatizar: por eso los motores maduros acaban ofreciendo avisos en desarrollo para lecturas fuera de ámbito y para mutaciones de objetos que deberían ser inmutables. Y es también la razón por la que la propuesta de TC39 es tan austera: cuanto más pequeña sea la superficie de funciones por las que hay que pasar, menos formas hay de esquivarlas por accidente.
Lo que llevas del nivel
El tracking automático es una variable de módulo, un patrón de guardar y restaurar, y una operación de desatado. Con eso descubre las dependencias exactas de cada ejecución, atraviesa fronteras de función, se adapta a las condicionales y falla exactamente en los cuatro sitios donde su mecanismo no llega.
Lo que aún no hemos decidido es qué hacer cuando una fuente cambia: si empujar el cambio hacia adelante, esperar a que alguien pregunte, o algo intermedio. Ese es el nivel 4.
- Escribe un caso mínimo para cada uno de los cuatro y comprueba que ninguno produce un error.
- Para cada uno, aplica el remedio y verifica que la reactividad se restablece.
- Añade una condición de carrera al caso asíncrono reejecutando el efecto durante la petición.
- Escribe la regla de diagnóstico de una sola frase con tus palabras y compruébala contra los cuatro casos.