wandres.dev
LIFECYCLE Y REFS · onMount, ciclo de vida

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.

⏱ 13 min

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.

🎯 Al terminar esta lección sabrás
  • Capturar un nodo con la variable local ref y con la forma de callback ref={el => ...}.
  • Entender cómo el compilador convierte ref en una asignación durante la creación del nodo.
  • Fijar el timing exacto: el ref se resuelve al crear, no al insertar en el documento.
  • Contrastar con React y conocer el patrón de ref reactivo 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.

⚠️
Solid no llama al ref con null al desmontar

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.

El ref pertenece a la creación; el uso pertenece al montaje

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.

⚔️ Captura, mide y reacciona
  1. Declara let el!: HTMLDivElement y, dentro de onMount, imprime el.offsetWidth; imprime lo mismo justo al asignar el ref en un callback y confirma que ahí da cero.
  2. Enfoca un input al montar usando la variable ref y onMount.
  3. Usa un callback ref dentro de un For para guardar cada li en un Map por su id.
  4. Reemplaza una variable ref por un setter de signal y dispara un efecto que corra en cuanto el nodo exista.
  5. Explica por qué leer la variable ref en el cuerpo del componente devuelve undefined.