wandres.dev
EL COMPILADOR DE SOLID · jsx-dom-expressions

Leer y depurar el output: por qué reacciona (o no) tu componente

La destreza práctica que cierra el nivel: abrir el JavaScript generado y diagnosticar la reactividad leyéndolo. Dónde ver el output —el Playground de Solid, el módulo transformado que sirve Vite, el REPL de Babel—, un checklist para reconocer de un vistazo qué es estático y qué reactivo, y el diagnóstico de los dos fallos canónicos: el valor congelado porque se leyó una vez fuera de un getter o de una función, y la falsa expectativa de que el cuerpo del componente se re-ejecute. Aprender a leer `insert`, `effect` y los getters de `createComponent` como la verdad del framework.

⏱ 16 min

Cierras el nivel con la destreza que lo vuelve útil a diario: abrir el output compilado y diagnosticar la reactividad leyéndolo. Cuando un valor no se actualiza o se actualiza de más, la respuesta no está en la documentación sino en el JavaScript generado, escrito en un lenguaje más honesto que el JSX. Verás dónde encontrar ese output, cómo reconocer de un vistazo qué quedó estático y qué reactivo, y cómo los dos bugs más comunes de Solid —el valor congelado y la falsa expectativa de re-ejecución— se ven a simple vista una vez sabes qué buscar.

🎯 Al terminar esta lección sabrás
  • Localizar el output compilado en el Playground, en el módulo que sirve Vite y en el REPL de Babel.
  • Reconocer en el output las marcas de lo estático —cadenas de template, valores planos— y de lo reactivo —accessors, effect, getters—.
  • Diagnosticar el bug del valor congelado leído una sola vez fuera de una función o de un getter.
  • Diagnosticar la falsa expectativa de que el cuerpo del componente se re-ejecuta.

Dónde ver el output

La herramienta más directa es el Playground oficial en playground.solidjs.com: escribes JSX en un panel y la pestaña Output muestra el JavaScript compilado en vivo, ideal para experimentar sin montar nada. En un proyecto real, abre las herramientas de red del navegador y localiza tu módulo .tsx: Vite lo sirve ya transformado a JavaScript, con las llamadas a template, insert y effect visibles. Y si quieres el proceso a mano, babel-preset-solid es un preset de Babel corriente que puedes invocar en un script para transformar una cadena y leer el resultado.

import { transform } from "@babel/core";
const salida = transform(codigoFuente, { presets: ["solid"] }).code;
console.log(salida); // el mismo JS que veria el navegador

Conviene tener a mano las tres vías porque sirven a momentos distintos. El Playground es para aprender y aislar un caso mínimo; el módulo que sirve Vite es para depurar un bug real en su contexto; y el REPL de Babel es para automatizar comprobaciones o inspeccionar cómo cambia el output al tocar una opción del preset. Las tres muestran el mismo artefacto: el código que de verdad corre.

Un truco de higiene antes de leer: aísla el componente sospechoso en el Playground reducido a lo mínimo que reproduce el fallo. El output de un módulo real trae ruido —imports, otros componentes, claves de hidratación— que estorba; un caso de diez líneas produce diez de output y el problema salta a la vista. Depurar reactividad es, casi siempre, comparar el output que tienes con el que esperabas.

Leer el output: un checklist de reconocimiento

Al abrir un módulo compilado, barre estas señales en orden y tendrás el mapa de lo que reacciona.

🧊

Cadena de template

Lo que aparece dentro de template(...) es estático: se horneó porque el compilador lo probó constante.

💉

insert con función

insert(el, accessor) con una función o accessor es reactivo; insert(el, valor) con un valor plano está congelado.

🔁

effect alrededor

Un atributo dentro de un effect(() => ...) es reactivo; el mismo atributo asignado fuera de un efecto se lee una vez.

🔑

getter en createComponent

get prop() { return x(); } preserva la reactividad de la prop; un prop: valor la entrega congelada.

La regla se resume en una pregunta: ¿el valor viaja como función o como dato? Si el compilador ve una función —un accessor, un getter, un thunk—, puede releerla y hay reactividad. Si ve un dato ya calculado, lo escribe una vez y ahí muere. Toda la depuración de reactividad en Solid se reduce, al final, a localizar en qué punto una función que debía llegar viva se convirtió en un dato inerte.

Por qué esto NO reacciona

El bug canónico es congelar un valor leyéndolo una sola vez, fuera de la función que el compilador necesitaba ver. Ocurre al destructurar props o al volcar una prop en una constante.

// mal: lee el getter UNA vez y guarda el dato
function Saludo(props: { nombre: string }) {
  const { nombre } = props;
  return <span>{nombre}</span>;
}
// output: el span recibe un dato plano, no un accessor -> congelado
const nombre = props.nombre;              // el getter se leyo aqui, una vez
_$insert(_el$, nombre);                    // sin funcion: nunca se relee

Compáralo con la versión que preserva el acceso hasta el insert. El compilador conserva el getter y el span reacciona:

// bien: el acceso a la prop llega vivo al JSX
function Saludo(props: { nombre: string }) {
  return <span>{props.nombre}</span>;
}
// output: el span recibe una funcion -> reactivo
_$insert(_el$, () => props.nombre);        // se relee cuando nombre cambia

El mismo error aparece con signals: const n = count() guarda un número y {n} compila a insert(el, n), congelado; mientras que {count()} compila a insert(el, count), vivo. Y aparece con stores, donde el output es igual de elocuente.

// mal: destructurar el store lee sus getters una vez
const { usuario } = store;   // dato plano
// bien: leer la propiedad en el punto de uso conserva el getter
<span>{store.usuario}</span>;
// output del caso bueno: el acceso al store llega vivo
_$insert(_el$, () => store.usuario);   // se relee cuando usuario cambia

El proxy del store, igual que las props, entrega getters que hay que ejecutar donde se usan. En todos los casos el output te lo grita: si ves un dato donde esperabas una función, ahí está la fuga de reactividad.

Por qué esperabas que reaccionara

El segundo malentendido no es un valor congelado sino una expectativa equivocada sobre el modelo de ejecución: creer que el cuerpo del componente se re-ejecuta. No lo hace —corre una vez— y el output lo demuestra, porque la función del componente no tiene ningún bucle ni se reinvoca: solo sus efectos e insert se re-ejecutan.

// mal: doble se calcula una vez, en la creacion
function Contador(props: { count: () => number }) {
  const doble = props.count() * 2;   // numero plano, calculado al crear
  return <span>{doble}</span>;       // insert(el, doble): congelado
}
// bien: doble es una funcion que el insert puede releer
function Contador(props: { count: () => number }) {
  const doble = () => props.count() * 2;  // derivacion reactiva
  return <span>{doble()}</span>;          // insert(el, doble): vivo
}

Al leer el output del primer caso no encontrarás ningún effect alrededor de doble: es una constante numérica cerrada en el closure. La cura es siempre la misma —envolver el cálculo en una función— y la confirmación siempre es la misma: en el output aparece ahora un accessor donde antes había un número. Este diagnóstico cubre una familia entera de sorpresas, desde el console.log que solo imprime una vez hasta la variable derivada que se queda clavada en su valor inicial.

Un console.log bien colocado es, de hecho, el atajo más rápido para interiorizar dónde vive la repetición.

function Panel(props: { abierto: () => boolean }) {
  console.log("cuerpo", props.abierto());                     // imprime UNA vez, al crear
  createEffect(() => console.log("effect", props.abierto())); // imprime en cada cambio
  return <span>{props.abierto() ? "abierto" : "cerrado"}</span>;
}

El log del cuerpo corre una sola vez porque el cuerpo no se repite; el de dentro del createEffect corre en cada cambio porque los efectos sí se re-ejecutan. Si esperabas que el primero se repitiera, el modelo de ejecución que tenías en la cabeza era el de React, no el de Solid.

El output es la verdad; el JSX es solo la intención

La madurez con Solid llega el día que dejas de discutir con el framework y empiezas a consultarlo. Ante cualquier duda sobre por qué algo reacciona o se queda inmóvil, la tentación del principiante es releer la documentación, revisar el modelo mental o preguntar en un foro; la costumbre del experto es abrir la pestaña de output y leer el JavaScript generado, porque ahí no hay opinión ni ambigüedad, solo el código exacto que el navegador ejecutará. Y en ese código la reactividad tiene una firma inconfundible que ya sabes reconocer: donde viaja una función —un accessor suelto, un getter en createComponent, un thunk dentro de un insert— hay vida, porque algo puede releerse; donde viaja un dato ya calculado hay quietud, porque el valor se escribió una vez y nadie volverá a mirarlo. Los dos errores que arruinan más tardes de depuración se reducen a esta única lectura. El valor congelado es un dato donde esperabas una función, casi siempre porque destructuraste una prop, un store o un signal en una constante y, al hacerlo, ejecutaste el getter una vez y tiraste la suscripción. La falsa expectativa de re-render es creer que el cuerpo del componente se repite, cuando el output enseña sin lugar a dudas que la función corre una sola vez y solo sus efectos vuelven. Interioriza que el JSX es la intención y el output es la verdad, y que entre ambos media un compilador que no miente: cuando los dos discrepen, el compilador tiene razón, y tu trabajo es leer su versión hasta entender qué le pediste de verdad. Esa lectura, que al principio cuesta, acaba siendo instantánea, y con ella Solid deja de tener misterios.

⚔️ Depura leyendo el output
  1. Pega en el Playground el Saludo que destructura props y confirma en la pestaña Output que el span recibe un dato plano; arréglalo y verifica que ahora recibe una función.
  2. Escribe const n = count() y {n}, observa el insert(el, n) congelado, y cámbialo a {count()} para ver aparecer insert(el, count).
  3. Calcula const doble = props.count() * 2 fuera de una función, comprueba que no hay effect alrededor, y conviértelo en () => ... para que el output muestre un accessor.
  4. Destructura una propiedad de un store con const { x } = store y localiza en el output el valor plano; léela como store.x en el punto de uso y comprueba que reaparece el getter.
  5. Ante un bug real de tu proyecto en el que algo no se actualiza, abre el módulo transformado en las herramientas de red y encuentra el punto exacto donde una función se convirtió en dato.