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

Closures y captura de valores

Por qué leer un signal y guardarlo en una const lo congela para siempre, y por qué en Solid siempre se lee por función. El corazón del modelo de ejecución y la fuente de casi todos los bugs de reactividad.

⏱ 10 min

Esta es la trampa que atrapa a todo desarrollador de React y la lección que, una vez entendida, hace que Solid encaje. Como el cuerpo corre una sola vez, cualquier valor que extraigas de un signal y guardes en una const es una foto fija: congelada en el instante en que corrió el cuerpo. La reactividad viaja por llamadas a función, no por variables. La disciplina se resume en una frase: lee tarde, lee llamando.

🎯 Al terminar esta lección sabrás
  • Entender por qué una const que guarda signal() queda congelada.
  • Distinguir leer el valor (n()) de pasar la fuente (n).
  • Aplicar la regla: leer lo más tarde posible, siempre dentro de una función.
  • Reconocer el patrón en props, efectos y manejadores de eventos.

Congelar frente a leer tarde

El bug y su arreglo, uno al lado del otro:

function Roto() {
  const [n, setN] = createSignal(0);
  const valor = n(); // captura 0 AHORA, para siempre

  return (
    <button onClick={() => setN(n() + 1)}>
      {valor}
    </button>
  );
}

function Bien() {
  const [n, setN] = createSignal(0);

  return (
    <button onClick={() => setN(n() + 1)}>
      {n()}
    </button>
  );
}

const valor = n() ejecuta n() una vez, durante la única corrida del cuerpo, y guarda el primitivo 0. A partir de ahí valor ya no tiene ninguna conexión con el signal: es un número muerto. {n()} en el JSX, en cambio, es una llamada que ocurre dentro del efecto que generó el compilador, y se vuelve a ejecutar cada vez que n cambia. Misma sintaxis de lectura, destinos opuestos: uno se ejecuta en el plano, el otro dentro del grafo.

La regla de oro: pasa la función, no el valor

Los paréntesis son el acto de “leer ahora”. Retrásalos. Cuando quieras compartir reactividad, pasa la fuente (n), no la lectura (n()), o léela justo en el punto de uso.

// pasar la fuente: el consumidor lee cuando quiera, sigue vivo
<Hijo valor={n} />
// dentro del hijo: props.valor() -> lectura reactiva

Aquí hay un matiz que conviene precisar para 2026. En el JSX y en las props, el compilador ya envuelve tus expresiones en getters, así que valor={n()} también sigue vivo: props.valor re-evalúa la expresión n() cada vez que se accede. El pecado no es la sintaxis de la prop, sino sacar la lectura a una const del cuerpo o destructurar las props, porque eso ejecuta la lectura una vez y guarda un valor muerto.

⚠️
Nunca destructures props

const { valor } = props o function Hijo({ valor }) leen cada prop una sola vez, al montar, y rompen la reactividad. Recibe siempre props entero y accede a props.valor en el punto de uso. Si necesitas separar o combinar, usa splitProps y mergeProps, que preservan los getters.

Dónde muerde: props, efectos y manejadores

flowchart TD
A[signal n] --> B[como lo usas]
B --> C[capturar en una const del cuerpo]
B --> D[leer dentro de una funcion]
C --> E[valor muerto congelado]
D --> F[lectura reactiva viva]
F --> G[el efecto se re-suscribe al leer]
style E fill:#f38ba8,color:#11111b
style F fill:#a6e3a1,color:#11111b

El mismo principio explica tres situaciones que parecen distintas:

  • Props: nunca destructures. Lee props.valor en el punto de uso; usa splitProps para separar lo local de lo que reenvías.
  • Efectos: lee el signal dentro del createEffect para que el efecto se suscriba. Si lo lees fuera y pasas el valor, el efecto no rastreará nada.
  • Manejadores: leer dentro del handler (onClick={() => setN(n() + 1)}) toma el valor vigente en el momento del clic, que es justo lo que quieres.
import { splitProps } from "solid-js";

function Boton(props) {
  // NO: const { variante, ...resto } = props  -> congela y pierde reactividad
  const [local, resto] = splitProps(props, ["variante"]);
  return <button class={local.variante} {...resto} />;
}

Un corolario práctico: en un manejador de evento, lee el signal dentro del manejador, no lo captures al declararlo. Así siempre operas sobre el valor vigente en el momento del clic.

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

// MAL: captura n al crear el handler, se queda con el valor inicial
const inc = ((v) => () => setN(v + 1))(n());

// BIEN: lee n cuando se dispara el evento
const inc2 = () => setN(n() + 1);

Lo mismo vale dentro de un efecto: si lees el signal fuera y pasas el valor, el efecto no se suscribe; si lo lees dentro, sí. La lectura tardía no es preferencia de estilo, es la condición para que exista la arista en el grafo.

Este es el porqué último de toda la regla: el grafo no se declara, se teje leyendo, y solo se teje donde y cuando lees.

📝
Un mismo signal, muchas lecturas

Nada te obliga a leer un signal una sola vez. Puedes llamar n() en el JSX, en tres efectos y en dos manejadores: cada lugar crea su propia suscripción o su propia lectura puntual, todas coherentes y siempre al día. El antipatrón no es leer mucho, es leer pronto y guardar el resultado en una const.

El caso de los stores: getters hasta el fondo

Con createStore la regla no cambia, solo se vuelve más sutil: el acceso a una propiedad anidada es en sí mismo una lectura rastreable. Por eso tampoco se destructura un store, y por eso pasar estado.usuario a un hijo y leer allí props.usuario.nombre mantiene la reactividad hasta la hoja del árbol.

import { createStore } from "solid-js/store";

const [estado, setEstado] = createStore({ usuario: { nombre: "Ada" } });

// MAL: captura el string ahora, se congela
const nombre = estado.usuario.nombre;

// BIEN: lee en el punto de uso, dentro del JSX o de un efecto
<h1>{estado.usuario.nombre}</h1>

La lección general es que en Solid casi nada es “un valor”: casi todo es “un acceso que, al ejecutarse, lee y se suscribe”. Un signal es una función; una propiedad de store es un getter; una prop es un getter. Congelar cualquiera de ellos en una const del cuerpo lo desconecta del grafo.

Es el mismo fenómeno que el clásico bug del bucle con var en JavaScript, trasladado a la reactividad: capturar demasiado pronto un valor que aún iba a cambiar. La cura también es la misma que allí se logró con let: mover la lectura al momento en que de verdad se necesita.

ℹ️
El linter eslint-plugin-solid te avisa

Muchos de estos errores —destructurar props, capturar un signal en el cuerpo— los detecta eslint-plugin-solid con la regla reactivity. No sustituye entender el modelo, pero convierte el “¿por qué no se actualiza?” en un aviso en el editor antes de ejecutar nada. Actívalo desde el primer día.

Los paréntesis son un acto, no una notación

En Solid n y n() son cosas radicalmente distintas, y todo el modelo pende de esa diferencia. n es el signal: un cable vivo, una fuente que puedes entregar a cualquiera para que la lea cuando la necesite. n() es el acto de leerla ahora mismo, en este instante, en esta línea. Cuando escribes const v = n() en el cuerpo, realizas ese acto una sola vez —durante la única ejecución del cuerpo— y embotellas el resultado. Desde entonces v es un cadáver: el número 0, desconectado de la fuente que un día lo produjo. La disciplina que Solid te pide es casi física: retrasa los paréntesis. No leas en el cuerpo; lee dentro de la función que el grafo llamará —la expresión del JSX, el efecto, el manejador—. Pasa fuentes (n), no lecturas (n()), y cuando debas leer, hazlo lo más tarde posible, en el punto exacto de uso. Domina este único reflejo y cada “¿por qué no se actualiza?” de Solid se disuelve, porque todos son, sin excepción, el mismo bug: leíste demasiado pronto y te quedaste con el cadáver.

⚔️ Caza el cadáver
  1. Reproduce el bug: guarda const v = n() en el cuerpo, muéstralo e incrementa n. Confirma que v no cambia mientras {n()} sí.
  2. Destructura props en un hijo (const { x } = props) y comprueba que pierdes la reactividad; arréglalo leyendo props.x en el punto de uso.
  3. Usa splitProps para separar props locales de las que reenvías con el spread, sin romper la reactividad.
  4. Formula la regla de oro con tus palabras y aplícala a un createEffect que deba re-suscribirse al cambiar un signal.
  5. Activa eslint-plugin-solid y provoca a propósito una destructuración de props; confirma que la regla reactivity te avisa antes de ejecutar.