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.
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.
- Entender por qué una
constque guardasignal()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.
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.valoren el punto de uso; usasplitPropspara separar lo local de lo que reenvías. - Efectos: lee el signal dentro del
createEffectpara 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.
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.
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.
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.
- Reproduce el bug: guarda
const v = n()en el cuerpo, muéstralo e incrementan. Confirma quevno cambia mientras{n()}sí. - Destructura props en un hijo (
const { x } = props) y comprueba que pierdes la reactividad; arréglalo leyendoprops.xen el punto de uso. - Usa
splitPropspara separar props locales de las que reenvías con el spread, sin romper la reactividad. - Formula la regla de oro con tus palabras y aplícala a un
createEffectque deba re-suscribirse al cambiar un signal. - Activa
eslint-plugin-solidy provoca a propósito una destructuración de props; confirma que la reglareactivityte avisa antes de ejecutar.