Saltar al contenido

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.

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.

JS
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.

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.

TSX
// mal: lee el getter UNA vez y guarda el datofunction Saludo(props: { nombre: string }) {  const { nombre } = props;  return <span>{nombre}</span>;}
JS
// output: el span recibe un dato plano, no un accessor -> congeladoconst 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:

TSX
// bien: el acceso a la prop llega vivo al JSXfunction Saludo(props: { nombre: string }) {  return <span>{props.nombre}</span>;}
JS
// 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.

TSX
// mal: destructurar el store lee sus getters una vezconst { usuario } = store;   // dato plano// bien: leer la propiedad en el punto de uso conserva el getter<span>{store.usuario}</span>;
JS
// 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.

TSX
// mal: doble se calcula una vez, en la creacionfunction Contador(props: { count: () => number }) {  const doble = props.count() * 2;   // numero plano, calculado al crear  return <span>{doble}</span>;       // insert(el, doble): congelado}
TSX
// bien: doble es una funcion que el insert puede releerfunction 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.

TSX
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.