El modelo mental completo de Solid
Todo lo que aprendiste por separado —el compilador que convierte JSX en DOM real, el grafo reactivo de sources y observers, y el árbol de ownership que libera recursos— es en realidad una sola máquina. Esta lección los funde en una única imagen mental: el compilador emite, en tiempo de ejecución, computaciones que son a la vez aristas del grafo de dependencias y nodos del árbol de propiedad. Comprender que esos tres sistemas son tres vistas de lo mismo es lo que separa a quien usa Solid de quien lo entiende de verdad.
Tres sistemas, una sola máquina
Escribe una línea trivial de JSX y sigue su destino. El compilador dom-expressions la parte en dos mitades: lo estático, que se clona de una plantilla HTML una sola vez, y lo dinámico, cada expresión que lee valores que cambian. Para lo estático emite un template; para lo dinámico emite una computación que reevalúa esa expresión y parchea el nodo concreto.
// Lo que escribesfunction Contador() { const [n, setN] = createSignal(0); return <p class="c">Total: {n()}</p>;}// A lo que compila (esencia, simplificada)const _tmpl = template(`<p class="c">Total: `);function Contador() { const [n, setN] = createSignal(0); const root = _tmpl(); // clona el DOM estatico una vez insert(root, n); // crea una computacion para lo dinamico return root;}Ese insert es la bisagra donde los tres sistemas se tocan. Por dentro crea un efecto de render: una computación que, al ejecutarse, lee n() y escribe el nodo de texto.
Y aquí está la clave que no se ve a simple vista: esa misma computación entra a la vez en los otros dos sistemas. Al leer n() se registra como observador de ese signal —eso es el grafo—; y al crearse dentro del cuerpo del componente, queda bajo el dueño de ese componente —eso es el ownership—. Una sola computación, nacida de una sola línea compilada, pertenece a los dos árboles a la vez. El compilador no escribió «reactividad» por un lado y «limpieza» por otro: escribió una pieza que es ambas cosas.
El recorrido de un cambio, de punta a punta
Ahora ejecuta setN(1) mentalmente y no sueltes el hilo. El setter no toca el DOM; escribe el valor en el signal y recorre su lista de observadores marcándolos como sucios. Nuestra computación de render está en esa lista, así que se reencola. Cuando el sistema drena la cola —en el siguiente microtask, o de inmediato si no hay batch—, reevalúa solo esa computación: vuelve a leer n(), obtiene 1, y actualiza el nodo de texto.
Detente en lo que no ocurrió: ni un árbol reconciliado, ni un diff, ni un componente reejecutado. El cuerpo de Contador corrió una única vez en su vida; lo que vuelve a correr es la computación diminuta que el compilador plantó para esa interpolación. Esa asimetría —el componente es andamiaje de un solo uso, las computaciones son lo vivo— es el corazón del modelo, y es exactamente lo que un desarrollador de React tarda más en interiorizar.
La precisión no es magia: es la consecuencia directa de que el tracking sea automático y exacto. Durante la ejecución de una computación hay un «listener actual»; cada signal que se lee mientras ese listener está activo lo apunta como observador. Por eso no necesitas listas de dependencias: la dependencia se descubre leyendo. Y por eso destructurar props o leer fuera del scope reactivo rompe la reactividad —no porque Solid sea caprichoso, sino porque leíste el valor cuando no había ningún listener escuchando, y el grafo nunca supo que dependías de él.
// El listener actual es el mecanismo del tracking automaticocreateEffect(() => { // mientras corre este cuerpo, ESTE efecto es el listener actual console.log(n()); // n queda suscrito a este efecto: arista del grafo}); // al terminar, deja de ser el listenerLos memos ocupan el centro del grafo y revelan que los papeles no son fijos: un createMemo es observer y source a la vez. Observa los signals que lee para saber cuándo recalcular, y a su vez es observado por quien lee su valor. Esa doble naturaleza es lo que permite encadenar derivaciones en capas: cada memo es un nodo intermedio que propaga hacia arriba solo si su resultado cambió, cortando el flujo cuando el valor derivado se mantiene igual.
// un memo es nodo intermedio: observa a n, y es observado por el JSXconst doble = createMemo(() => n() * 2); // observer de n, source para quien lo lea// si n pasa de 3 a 3 (mismo valor), doble no recalcula ni propaga: corta el grafoOwnership: el eje perpendicular al grafo
Aquí está la confusión que hay que disolver. El grafo y el ownership parecen el mismo árbol, y no lo son: son ejes perpendiculares.
El grafo conecta una computación con los signals que lee, estén donde estén —un memo puede depender de un signal creado a diez componentes de distancia—. El ownership, en cambio, conecta una computación con el scope donde nació, sin importar qué lea ni de dónde. Un mismo efecto tiene, a la vez, aristas de dependencia que apuntan lateralmente hacia sus fuentes y una arista de propiedad que apunta hacia arriba, a su dueño. Confundir los dos ejes es la raíz de casi todos los malentendidos sobre por qué algo se limpia o no se limpia cuando esperabas.
Esa perpendicularidad es lo que hace posible la limpieza automática. Cuando un componente se desmonta, Solid dispone su dueño; la disposición recorre el árbol de ownership hacia abajo ejecutando cada onCleanup y desconectando cada computación de sus fuentes en el grafo. No tienes que desuscribir nada a mano porque la estructura de propiedad ya sabe qué nació dentro de qué.
La frase que fija la distinción: el grafo dice «quién depende de quién»; el ownership dice «quién vive dentro de quién». Los recursos se liberan por el segundo, las actualizaciones fluyen por el primero, y una misma computación participa en ambos sin que sus dos roles se estorben. Ver esos dos árboles superpuestos sobre tu código —uno lateral, uno vertical— es literalmente ver la máquina funcionar.
// Dos ejes en una misma computacionfunction Panel() { // <- dueno: el componente Panel const [tema, setTema] = createSignal("claro"); createEffect(() => { // nace bajo el dueno Panel (ownership) document.body.dataset.tema = tema(); // lee tema: arista de grafo hacia tema onCleanup(() => { // cleanup atado al dueno Panel delete document.body.dataset.tema; }); }); // al desmontar Panel: dispose baja por ownership y corre el onCleanup, // ademas de cortar la arista de grafo hacia tema. Un solo mecanismo.}Que los dos ejes sean explícitos se vuelve tangible cuando el ownership se rompe: si arrancas trabajo asíncrono, el dueño puede haber desaparecido al volver, y onCleanup ya no tendría dónde engancharse. Para esos casos Solid expone el eje de propiedad como una API de primera clase —getOwner y runWithOwner— con la que capturas el dueño actual y reejecutas bajo él más tarde.
import { getOwner, runWithOwner, onCleanup } from "solid-js";function ConTimeout() { const owner = getOwner(); // captura el nodo de ownership actual setTimeout(() => { runWithOwner(owner, () => { // reengancha al arbol de propiedad const id = setInterval(tick, 1000); onCleanup(() => clearInterval(id)); // ahora SI se ata al dueno correcto }); }, 500);}Que exista esa API es la prueba de que el ownership no es un detalle interno sino una estructura real que puedes leer y manipular. El grafo lo tocas con untrack, on y batch; el ownership, con getOwner, runWithOwner y onCleanup. Dos ejes, dos juegos de herramientas, una sola máquina.