wandres.dev
EL MODELO DE EJECUCIÓN · el componente corre una vez

Dónde vive la reactividad

La reactividad no vive en el cuerpo del componente, sino en el JSX y en los efectos. El cuerpo solo cablea el grafo: declara nodos y conexiones que otro motor ejecutará por su cuenta.

⏱ 10 min

Si el cuerpo corre una sola vez, ¿dónde ocurre la vida? En dos sitios, y solo dos: dentro del JSX (que el compilador convierte en efectos de grano fino) y dentro de los efectos que tú escribes con createEffect, createMemo o createRenderEffect. Todo lo demás —el cuerpo— es cableado inerte: dibuja el grafo y se aparta. Localizar esos dos barrios reactivos es lo que separa a quien pelea con Solid de quien lo entiende.

🎯 Al terminar esta lección sabrás
  • Localizar los dos únicos lugares reactivos: el JSX y los efectos.
  • Entender que el cuerpo solo declara nodos y conexiones del grafo.
  • Ver cómo el compilador convierte cada expresión {expr} del JSX en un efecto.
  • Saber cuándo hace falta un createEffect explícito y cuándo basta una función.

El cuerpo cablea; el grafo ejecuta

Piensa en el cuerpo como el plano de un circuito. Dibujar el plano —ejecutar el cuerpo— ocurre una vez. La corriente que luego lo recorre —la reactividad— es otra cosa distinta y corre cada vez que cambia un signal.

function Perfil(props) {
  const [activo, setActivo] = createSignal(false);
  // el cuerpo: solo declaraciones. Nada de aqui "reacciona".
  const clase = () => (activo() ? "on" : "off"); // una funcion derivada

  return (
    <div class={clase()}>
      <span>{props.nombre}</span>
      <button onClick={() => setActivo(!activo())}>toggle</button>
    </div>
  );
}

Lo único reactivo son las expresiones del JSX: class={clase()} y {props.nombre}. El compilador envuelve cada una en su propio efecto, que se re-ejecuta cuando cambian sus dependencias. La función clase del cuerpo es solo una declaración; no “corre” hasta que el JSX la llama dentro de un efecto. El cuerpo no reacciona: describe quién reaccionará.

El compilador convierte el JSX en efectos

Cada expresión dinámica del JSX se compila a un efecto dedicado que lee los signals que toca, se suscribe a ellos y escribe el resultado en un nodo del DOM:

flowchart LR
J[expresion en el JSX] --> C[compilador de solid]
C --> E[efecto dedicado]
E --> N[nodo del DOM]
S[signal] -.suscribe y notifica.-> E
style E fill:#cba6f7,color:#11111b
style N fill:#a6e3a1,color:#11111b

Conceptualmente, lo que escribes se transforma en algo así:

// lo que escribes
<span>{props.nombre}</span>

// lo que genera el compilador (idea aproximada)
const span = document.createElement("span");
createRenderEffect(() => (span.textContent = props.nombre));

Por eso no necesitas useMemo para el DOM: la granularidad ya es de una expresión por efecto. Cambiar props.nombre re-ejecuta solo ese efecto de texto y actualiza solo ese nodo, sin tocar a sus hermanos ni reconciliar árbol alguno.

Hay además dos variantes según cuándo corre el efecto. createRenderEffect se ejecuta durante el render inicial, antes de pintar; es lo que el compilador usa internamente para el DOM. createEffect corre después del montaje, y es el que empleas para efectos hacia el exterior. La distinción importa cuando necesitas leer el DOM ya construido o evitar un parpadeo.

// corre durante el render, util para escribir el DOM
createRenderEffect(() => (el.className = clase()));

// corre despues del montaje, util para efectos hacia el exterior
createEffect(() => analitica.enviar(pagina()));

Efectos explícitos: cuándo sí

La regla es simple. Si el valor termina en el JSX, casi nunca necesitas un memo: el efecto del JSX ya lo rastrea. Reserva createMemo para derivados caros o compartidos, y createEffect para efectos que escapan del render: red, almacenamiento, suscripciones, sincronización con sistemas externos.

const [n, setN] = createSignal(2);

// derivacion barata: una funcion basta, se recalcula al leerse
const doble = () => n() * 2;

// derivacion cara o compartida: memoiza, se recalcula solo si n cambia
const pesado = createMemo(() => costoso(n()));

// efecto secundario: sincroniza con el mundo exterior
createEffect(() => localStorage.setItem("n", String(n())));

doble no cachea nada: cada lectura recalcula. Da igual si es barato. pesado sí memoiza: guarda el resultado y solo lo rehace cuando n cambia, y además notifica a sus consumidores una sola vez. createEffect no devuelve valor; existe por su efecto lateral y se re-ejecuta cuando cambia cualquier signal que leas dentro.

⚠️
No pongas efectos secundarios en el cuerpo

Escribir localStorage.setItem(...) directamente en el cuerpo lo ejecuta una vez, al montar, y nunca más. Si querías reaccionar a cambios, has puesto la lógica en el único lugar que no reacciona. Todo efecto secundario que deba repetirse va dentro de un createEffect; el cuerpo solo debe registrarlo, no ejecutarlo.

El grafo se re-suscribe en cada ejecución

Hay un detalle que cierra el modelo: las dependencias de un efecto no se declaran, se descubren, y se vuelven a descubrir cada vez que el efecto corre. Solid recolecta como dependencias exactamente los signals que se leyeron durante esa ejecución. Una rama no tomada no suscribe nada.

const [modo, setModo] = createSignal("a");
const [x, setX] = createSignal(1);
const [y, setY] = createSignal(2);

createEffect(() => {
  // se suscribe a modo, y ademas a x O a y segun el valor actual
  console.log(modo() === "a" ? x() : y());
});

Con modo en "a", el efecto lee modo y x, así que se suscribe a esos dos; y no se lee, y cambiar y no dispara nada. Cuando modo pasa a otro valor, en la siguiente corrida el grafo recolecta un conjunto distinto de dependencias. Esta suscripción dinámica es la razón última de la regla del nivel anterior: hay que leer dentro de la función, porque leer es, literalmente, suscribirse.

A veces quieres controlar eso a mano. on fija las dependencias de forma explícita y permite diferir la primera ejecución; untrack lee un signal sin suscribirse a él:

import { createEffect, on, untrack } from "solid-js";

// dependencias explicitas: solo reacciona a modo, no a lo que lea dentro
createEffect(on(modo, (m) => console.log("modo cambio a", m), { defer: true }));

createEffect(() => {
  const actual = x();               // se suscribe a x
  const cfg = untrack(() => y());   // lee y SIN suscribirse
  console.log(actual, cfg);
});

Este rastreo por ejecución es lo que hace que Solid no necesite arrays de dependencias: la lista de dependencias es el conjunto de signals leídos en la última corrida, y se recalcula sola en cada una.

ℹ️
untrack no rompe la reactividad, la acota

untrack no es un truco para “apagar” Solid: es la herramienta para leer un valor puntual sin crear una arista en el grafo. Lo usas cuando un efecto necesita el valor actual de un signal pero no debe re-ejecutarse cuando ese signal cambie. Abusar de él esconde dependencias; usarlo con criterio evita bucles y re-ejecuciones espurias.

El cuerpo es un plano, no un motor

El error más profundo de quien llega de React es esperar que el cuerpo “vuelva a correr” para que su lógica se repita. Nunca lo hará. El cuerpo es un plano: declara qué signals existen, qué memos derivan de ellos, qué efectos los observan y qué DOM devuelve el componente. Una vez dibujado, el plano es inerte. El motor que le da vida es el grafo reactivo, y ese grafo solo habita en dos barrios: las expresiones del JSX que el compilador convirtió en efectos, y los efectos que escribiste a mano. Cualquier lógica que quieras que “reaccione” debe vivir dentro de uno de esos dos sitios —dentro de una función que el grafo llamará—, nunca como una sentencia suelta del cuerpo. Por eso const doble = n() * 2 se congela y const doble = () => n() * 2 respira: el primero es un valor calculado una vez mientras se dibujaba el plano; el segundo es un cable que el grafo puede tirar cuando necesite una lectura fresca.

⚔️ Cablea el grafo a mano
  1. Escribe un valor derivado como función (() => a() + b()) y úsalo en el JSX. Cambia a y confirma que el JSX se actualiza sin ningún createEffect.
  2. Sustituye la función por createMemo y observa que para el usuario el resultado es idéntico; razona cuándo merece la pena memoizar.
  3. Añade un createEffect que escriba en localStorage al cambiar un signal. Comprueba en las devtools que solo se dispara al cambiar su dependencia.
  4. Dibuja en papel el grafo de tu componente: nodos (signals, memos) y aristas (quién lee a quién). Marca qué vive en el JSX y qué en efectos.
  5. Escribe un createRenderEffect que ponga una clase en el DOM y compáralo con un createEffect equivalente; observa cuál corre antes de pintar.