Refs: variable, callback y forwarding
Cómo capturar un nodo del DOM en Solid con la variable local ref o el callback ref, por qué el elemento aún no está insertado cuando el ref se asigna y para eso existe onMount, y cómo reenviar un ref a través de un componente con la prop ref y tiparlo.
Tarde o temprano necesitas el nodo real: para enfocar un input, medir su tamaño, montar una librería de gráficos o arrancar un observador. En Solid eso se hace con ref, y hay un matiz que confunde a todo el que viene de React: el ref se asigna cuando el elemento se crea, que es antes de que esté insertado en el documento. Confundir “existe el nodo” con “está en el DOM” es la fuente número uno de mediciones que dan cero y focos que no funcionan.
- Capturar un nodo con la variable local
refy con el callbackref={el => ...}. - Entender la línea temporal: cuándo se asigna el ref y cuándo el nodo está en el documento.
- Usar
onMountpara trabajar con el elemento ya adjunto (medir, enfocar, observar). - Reenviar un ref a través de un componente con la prop
refy tiparlo correctamente.
Las dos formas de capturar
La forma más común es una variable local. El compilador de Solid reconoce ref={identificador} y, en lugar de leer la variable, genera código que le asigna el nodo al crearlo:
import { onMount } from "solid-js";
function Editor() {
let campo!: HTMLInputElement; // ! = definite assignment: TS confia en que se asignara
onMount(() => campo.focus()); // aqui el nodo ya esta en el DOM
return <input ref={campo} placeholder="escribe" />;
}
Se usa let, no const, porque Solid va a asignarla. El ! es la aserción de asignación definitiva de TypeScript: le prometes al compilador que estará asignada antes de usarla. La otra forma es un callback, útil cuando quieres reaccionar en el momento de la creación o registrar el nodo en una estructura (por ejemplo, en una lista):
<For each={items()}>
{(item) => <li ref={(el) => registrar(item.id, el)}>{item.nombre}</li>}
</For>
El callback recibe el elemento como argumento en cuanto se crea. Cada iteración de <For> obtiene su propio nodo, así que es el patrón natural para guardar referencias a elementos dinámicos.
Cuándo existe el DOM
Aquí está el matiz que hay que grabar. Tanto la variable como el callback se resuelven durante la creación del elemento, que ocurre antes de que su padre lo inserte en el documento vivo. En ese instante el nodo existe como objeto, pero el.parentNode puede ser null y cualquier medida de layout —offsetWidth, getBoundingClientRect()— da 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 y el callback se resuelven] R --> I[El padre lo inserta en el documento] I --> M[onMount se ejecuta con el DOM ya adjunto] style R fill:#fab387,color:#11111b style M fill:#a6e3a1,color:#11111b
La regla operativa es simple: usa el ref para capturar el nodo, y onMount para usarlo cuando ya está adjunto. onMount corre una sola vez, después del render inicial, cuando el árbol que devuelve el componente ya está insertado. Todo lo que dependa de estar en el DOM —enfocar, medir, arrancar un ResizeObserver o IntersectionObserver, inicializar una librería de terceros— va dentro de onMount, no junto a la asignación del ref.
function Lienzo() {
let canvas!: HTMLCanvasElement;
onMount(() => {
// Ya esta en el DOM: medir y dibujar tienen sentido aqui.
const rect = canvas.getBoundingClientRect();
const ctx = canvas.getContext("2d")!;
ctx.fillRect(0, 0, rect.width, rect.height);
});
return <canvas ref={canvas} />;
}
El mismo patrón sirve para observar el nodo: dentro de onMount ya está adjunto, y un onCleanup registrado ahí se dispara al desmontar.
import { onMount, onCleanup } from "solid-js";
function Panel() {
let caja!: HTMLDivElement;
onMount(() => {
const ro = new ResizeObserver(([entrada]) => medir(entrada.contentRect));
ro.observe(caja);
onCleanup(() => ro.disconnect()); // limpieza atada al ciclo de vida
});
return <div ref={caja} />;
}
En SSR no existe el DOM: los refs no se asignan y onMount no corre en el servidor, solo en el cliente. Todo código que toque el nodo —medir, enfocar, observar— debe vivir en onMount. Es otra razón para no leer el ref en el cuerpo del componente: ese cuerpo sí corre en el servidor, donde no hay nodo alguno que capturar.
El cuerpo de un componente Solid corre una sola vez, y lo hace antes de que el JSX se evalúe y los elementos se creen. Si lees la variable ref ahí —fuera de onMount o de un efecto— vale undefined. El nodo no existe todavía. Toda interacción con el elemento debe ocurrir dentro de onMount, de un createEffect, o de un manejador de eventos, nunca en la ejecución lineal del cuerpo.
Forwarding: el ref a través de un componente
Un componente no es un elemento del DOM, así que <MiCampo ref={x} /> no captura nada por sí solo: hay que reenviarlo. La clave es que Solid compila ref={variable} del lado del padre a un callback que asigna esa variable. Por tanto, dentro del componente hijo, props.ref siempre llega como una función que puedes pasar directamente al ref de un elemento nativo:
import type { JSX } from "solid-js";
function Campo(props: {
ref?: HTMLInputElement | ((el: HTMLInputElement) => void);
placeholder?: string;
}) {
// Reenvio directo: Solid maneja tanto la forma de variable como la de callback.
return <input ref={props.ref} placeholder={props.placeholder} />;
}
// Uso: el padre captura el input interno como si fuera propio.
function Formulario() {
let campo!: HTMLInputElement;
onMount(() => campo.focus());
return <Campo ref={campo} placeholder="correo" />;
}
Si el componente necesita su propio ref interno y además reenviar el del padre, compón ambos en un callback. props.ref es invocable (o undefined), así que basta llamarlo:
function Campo(props: { ref?: (el: HTMLInputElement) => void }) {
let interno!: HTMLInputElement;
return (
<input
ref={(el) => {
interno = el; // tu referencia local
(props.ref as ((e: HTMLInputElement) => void) | undefined)?.(el); // reenvio
}}
/>
);
}
Componer refs a mano es correcto pero repetitivo. mergeRefs de @solid-primitives/refs acepta el props.ref del padre y tu setter local y devuelve un único callback que actualiza ambos: ref={mergeRefs(props.ref, (el) => (interno = el))}. Es la forma ergonómica de tener referencia interna y forwarding a la vez sin escribir el casteo cada vez.
La idea que ordena todo este nivel es separar dos momentos que React fusiona en tu intuición: el instante en que el nodo se crea y el instante en que entra en el documento. En Solid el ref pertenece al primero —es un gancho que el compilador dispara durante la construcción del árbol, cuando el elemento es un objeto en memoria sin padre ni geometría— y onMount pertenece al segundo —cuando ese árbol ya cuelga de la página y las medidas, el foco y los observadores tienen sentido—. Casi todos los bugs de refs vienen 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 el cuerpo del componente que corre antes que el JSX. Y el forwarding no es un mecanismo aparte, sino la misma idea vista desde fuera: como ref={variable} compila a un callback, un componente solo tiene que aceptar esa función y entregarla a su elemento nativo, y la referencia atraviesa la frontera del componente sin ceremonia. Entender la línea temporal creación → inserción → montaje te da un modelo mental con el que las refs dejan de sorprenderte para siempre.
- Escribe un componente con
let el!: HTMLDivElementy, dentro deonMount, imprimeel.offsetWidth. Luego imprime lo mismo justo al asignar el ref y comprueba que da cero. - Enfoca automáticamente un
<input>al montar el componente usando la variable ref yonMount. - Usa un callback ref dentro de un
<For>para guardar cada<li>en unMappor su id. - Crea un componente
Campoque reenvíeprops.refa su<input>interno y captúralo desde el padre para enfocarlo. - Añade una referencia interna al componente anterior sin perder el forwarding, primero a mano y luego con
mergeRefs.