Refs al DOM: la variable, el callback y su timing
Cómo captura Solid un nodo del DOM con la variable local ref y con el callback ref, cómo el compilador transforma ref en una asignación durante la creación del elemento, y el timing exacto: el ref se resuelve al crear el nodo, antes de insertarlo, por lo que existe como objeto pero aún no está en la página. Diferencias de timing con React y el patrón ref reactivo con un setter de signal.
Tarde o temprano necesitas el nodo real: enfocar, medir, arrancar una librería. En Solid se captura con ref, y todo el nivel gira sobre un único matiz temporal que confunde a quien viene de React: el ref se resuelve cuando el elemento se crea, que es antes de que su padre lo inserte en el documento. Confundir “el nodo existe” con “el nodo está en la página” es la fuente número uno de mediciones que dan cero y focos que no funcionan. Dominar la línea temporal creación → inserción → montaje hace que las refs dejen de sorprenderte.
- Capturar un nodo con la variable local
refy con la forma de callbackref={el => ...}. - Entender cómo el compilador convierte
refen una asignación durante la creación del nodo. - Fijar el timing exacto: el
refse resuelve al crear, no al insertar en el documento. - Contrastar con React y conocer el patrón de
refreactivo con un setter de signal.
Dos formas de capturar, un mismo instante
La forma más común es una variable local. El compilador reconoce ref={identificador} y, en lugar de leer la variable, genera código que le asigna el nodo en cuanto lo crea:
import { onMount } from "solid-js";
function Editor() {
let campo!: HTMLInputElement; // ! promete a TS que se asignara antes de usarse
onMount(() => campo.focus()); // aqui el nodo ya esta en el documento
return <input ref={campo} placeholder="escribe" />;
}
Se usa let, no const, porque Solid va a escribir en la variable. La otra forma es un callback: recibe el elemento como argumento en el mismo instante de la creación. Es lo natural cuando quieres reaccionar al nacer el nodo o guardarlo en una estructura dinámica.
<For each={items()}>
{(item) => <li ref={(el) => registrar(item.id, el)}>{item.nombre}</li>}
</For>
Lo esencial es que ambas formas se resuelven en el mismo momento: cuando el elemento se construye. La variable y el callback no son dos timings distintos; son dos maneras de recibir el mismo nodo en el mismo punto de la línea temporal.
El compilador convierte ref en una asignación
ref no es una prop que el elemento reciba en runtime, sino una instrucción para el compilador. Cuando Solid ve ref={campo}, no evalúa la expresión como valor: la trata como un destino de escritura. Lo que emite se parece conceptualmente a esto:
// Lo que escribes:
<input ref={campo} />;
// Lo que el compilador emite, en esencia:
const _el = document.createElement("input");
campo = _el; // asignacion inmediata, en la creacion
// ... mas tarde el padre hace parent.appendChild(_el)
Por eso la variable debe ser un let y por eso ref={campo} y ref={(el) => ...} son la misma operación vista de dos formas: en el primer caso el compilador asigna a tu variable; en el segundo, invoca tu función con el nodo. En ambos, la línea que captura corre entre crear el elemento e insertarlo.
Crear no es insertar
Aquí está el matiz que hay que grabar. En el instante en que el ref se resuelve, el nodo existe como objeto en memoria, pero su parentNode puede ser null y cualquier medida de layout —offsetWidth, getBoundingClientRect— devuelve cero, porque el elemento todavía no forma parte de la página.
flowchart LR C[se crea el nodo en memoria] --> R[el ref o el callback se resuelven] R --> I[el padre inserta el nodo en el documento] I --> M[onMount corre con el DOM ya adjunto] style R fill:#fab387,color:#11111b style M fill:#a6e3a1,color:#11111b
Los cuatro instantes de la vida temprana de un nodo, en orden:
1 Creación
El runtime crea el nodo en memoria con document.createElement. Existe como objeto, sin padre ni geometría.
2 Asignación del ref
En ese mismo paso el compilador asigna el nodo a tu variable o invoca tu callback. Ya tienes el elemento, pero no está en la página.
3 Inserción
El padre lo mete en el documento con appendChild. Ahora parentNode existe y el layout es real.
4 onMount
Se vacía la cola de efectos y corre onMount. Medir, enfocar y observar por fin tienen sentido.
La regla operativa es tajante: usa el ref para capturar el nodo, y onMount para usarlo cuando ya está adjunto. Todo lo que dependa de estar en el documento —enfocar, medir, arrancar un ResizeObserver— va dentro de onMount, nunca junto a la asignación del ref.
function Lienzo() {
let canvas!: HTMLCanvasElement;
onMount(() => {
const rect = canvas.getBoundingClientRect(); // ya insertado: mide de verdad
const ctx = canvas.getContext("2d")!;
ctx.fillRect(0, 0, rect.width, rect.height);
});
return <canvas ref={canvas} />;
}
Leer el ref en el cuerpo del componente es el error simétrico: el cuerpo corre antes de que el JSX se evalúe, así que ahí la variable aún vale undefined. El nodo no existe todavía en ninguna forma.
Si vienes de React, esperas que el callback ref se invoque con el nodo al montar y con null al desmontar. Solid no hace lo segundo: el callback se llama una sola vez, con el elemento, en su creación, y nunca más. El teardown no viaja por el ref, sino por onCleanup. Así que no escribas lógica de limpieza dentro del callback esperando un segundo disparo con null: registra un onCleanup en onMount o en un efecto.
Cuando necesitas que el nodo sea reactivo —que otros cómputos reaccionen a que ya existe—, la variable let no basta, porque asignarla no notifica a nadie. El patrón entonces es pasar el setter de un signal directamente como ref:
import { createSignal, createEffect } from "solid-js";
function Panel() {
const [caja, setCaja] = createSignal<HTMLDivElement>();
createEffect(() => {
const el = caja();
if (!el) return; // undefined hasta que el nodo se crea y asigna
el.scrollIntoView();
});
return <div ref={setCaja}>...</div>;
}
Como setCaja es una función, encaja en la forma de callback: Solid lo invoca con el nodo al crearlo, el signal pasa de undefined al elemento, y el efecto que lo lee se re-ejecuta. Es la versión reactiva de la captura, útil cuando el “ya existe el nodo” debe propagarse por el grafo.
En resumen: usa la variable let cuando solo necesitas el nodo dentro de onMount, y usa el setter de signal cuando otros cómputos deben despertar en cuanto el nodo aparece. Ambos comparten el mismo instante de captura; se diferencian solo en si notifican al grafo o no.
La idea que ordena todo este nivel es separar dos instantes que tu intuición de React fusiona: cuando el nodo se crea y cuando entra en el documento. En Solid el ref pertenece al primero —es un gancho que el compilador dispara mientras construye el árbol, cuando el elemento es un objeto sin padre ni geometría— y onMount pertenece al segundo, cuando ese árbol ya cuelga de la página y medir, enfocar u observar tienen sentido. Casi todos los bugs de refs nacen de pedirle al primer momento lo que solo el segundo puede dar: medir antes de insertar, enfocar un nodo que aún no está en pantalla, leer la variable en un cuerpo que corre antes que el JSX. Y como ref es una directiva del compilador, no una prop de runtime, entiendes de golpe por qué exige un let, por qué la variable y el callback son el mismo timing, por qué un setter de signal encaja donde encaja un callback, y por qué no hay un segundo disparo con null: no hay ciclo de vida oculto que lo emita, solo una asignación en la construcción y una limpieza atada al dueño. Interioriza la recta creación → inserción → montaje y las refs pasan de ser una fuente de sorpresas a ser la parte más predecible del framework.
- Declara
let el!: HTMLDivElementy, dentro deonMount, imprimeel.offsetWidth; imprime lo mismo justo al asignar elrefen un callback y confirma que ahí da cero. - Enfoca un
inputal montar usando la variablerefyonMount. - Usa un callback
refdentro de unForpara guardar cadalien unMappor su id. - Reemplaza una variable
refpor un setter de signal y dispara un efecto que corra en cuanto el nodo exista. - Explica por qué leer la variable
refen el cuerpo del componente devuelveundefined.