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.
Has recorrido tres sistemas como si fueran capítulos independientes: la compilación que transforma tu JSX en creación de DOM real más expresiones reactivas; el grafo reactivo de signals que se propagan a los efectos y memos que dependen de ellos; y el árbol de ownership que ata cada recurso a su dueño y lo libera al desmontar. La revelación del nivel Dios es que no son tres cosas: son tres proyecciones de una única máquina. El compilador no genera “código y aparte reactividad”; genera código que, al ejecutarse, teje simultáneamente el grafo de dependencias y el árbol de propiedad. Cuando esa imagen se vuelve una sola, dejas de razonar caso por caso y empiezas a ver qué hace Solid.
- Unir compilación, grafo reactivo y ownership en una única imagen mental coherente.
- Trazar el recorrido completo de un cambio: desde el setter hasta el nodo exacto del DOM.
- Distinguir el eje del grafo (dependencia) del eje del ownership (propiedad), perpendiculares entre sí.
- Usar esa imagen para predecir, sin ejecutar, qué se actualiza y qué se libera.
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 escribes
function 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.
flowchart TD J[JSX en el editor] -->|compilador dom-expressions| T[plantilla estatica mas expresiones dinamicas] T --> R[render clona el DOM real una vez] R --> C[cada expresion dinamica es una computacion] C -->|lee signals mientras corre| G[GRAFO arista de dependencia] C -->|nace dentro de un scope| O[OWNERSHIP arista de propiedad] G -->|un setter notifica| U[reejecuta solo esa computacion] U --> P[parchea solo ese nodo del DOM] O -->|el dueno se dispone| D[cleanup recursivo hacia abajo] style C fill:#89b4fa,color:#11111b style G fill:#a6e3a1,color:#11111b style O fill:#f9e2af,color:#11111b
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 automatico
createEffect(() => {
// 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 listener
Los 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 JSX
const 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 grafo
El grafo de Solid combina dos fases para ser glitch-free. Cuando un signal cambia, empuja una marca de «sucio» por sus observadores sin recalcular nada todavía. El recálculo ocurre en la fase pull, en orden topológico, de modo que ningún memo se evalúa antes que sus fuentes ni se ve un estado intermedio inconsistente. Un memo que depende de dos signals que cambian en el mismo batch se recalcula una sola vez, no dos. Esta separación push/pull es la razón por la que Solid nunca muestra el «valor viejo» un instante: la propagación y la evaluación son pasos distintos.
Ownership: 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 computacion
function 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.
Compilación
Parte el JSX en estático (plantilla clonada una vez) y dinámico (una computación por expresión). No hay VDOM: el compilador ya decidió qué es reactivo.
Grafo
Sources y observers conectados por lectura. Push para invalidar, pull en orden topológico para evaluar. Actualiza el nodo exacto, glitch-free.
Ownership
Un árbol perpendicular al grafo: quién nació dentro de quién. Al disponer un dueño, corre cada cleanup y corta cada suscripción, en cascada.
La imagen que hay que grabar, la que unifica el track entero, es esta: cada trozo dinámico de tu interfaz se convierte, por obra del compilador, en una computación, y esa computación existe simultáneamente en dos estructuras que la mayoría confunde en una. En el grafo reactivo es un observador, conectado por lectura a los signals que consultó mientras corría; esas aristas se crean solas porque durante su ejecución la computación es el listener actual y cada signal leído la registra. En el árbol de ownership es un nodo hijo del scope donde se creó, conectado hacia arriba a su dueño sin que importe lo más mínimo qué lea. Los dos ejes son perpendiculares: el del grafo va de lado, hacia las fuentes, y sostiene las actualizaciones; el del ownership va hacia arriba, hacia el creador, y sostiene la limpieza. Cuando un setter dispara, el sistema empuja invalidación por el eje del grafo y, en la fase de pull, reevalúa en orden topológico solo las computaciones sucias, cada una parcheando su nodo exacto del DOM sin diffing ni reconciliación —porque el compilador ya resolvió, en build time, qué era estático y qué dinámico, y no queda nada que comparar en runtime—. Cuando un componente muere, el sistema baja por el eje del ownership disponiendo el subárbol, corriendo cada onCleanup y cortando cada arista de grafo que colgaba de esas computaciones, de modo que ni un listener ni un efecto sobreviven a su dueño. El componente que corre una vez, las props que no se destructuran, el signal que es una función, el onCleanup que se ata solo, el memo que nunca muestra un valor rancio, la ausencia total de VDOM: no son seis reglas que memorizar, son seis consecuencias de esta única arquitectura. Quien la ve como una sola imagen deja de preguntarse “qué pasa si…” y empieza a derivar la respuesta, porque ya no razona sobre un framework, razona sobre una máquina cuyas piezas conoce hasta el fondo.
- Toma un componente con una interpolación
{signal()}y describe, línea por línea, en qué compila: qué es plantilla estática y qué computación dinámica. - Traza
setN(1)de punta a punta: por qué el cuerpo del componente NO reejecuta, qué computación sí lo hace, y qué nodo exacto del DOM cambia. - Dibuja, para un efecto cualquiera, sus aristas de grafo (hacia qué signals lee) y su arista de ownership (bajo qué dueño nació), y comprueba que son perpendiculares.
- Explica por qué destructurar props rompe el grafo apelando al mecanismo del “listener actual”, no a una regla memorizada.
- Predice, sin ejecutar, qué
onCleanupcorren y qué suscripciones se cortan al desmontar un componente con dos efectos anidados; verifica tu predicción con las Devtools.