Eventos: delegados frente a nativos
Los dos sistemas de eventos de Solid: los delegados con onClick y su listener global en el document, los nativos con on:click, cuándo la delegación gana en rendimiento y cómo pasar datos al handler sin crear closures por fila.
Dos formas de escuchar
El compilador JSX de Solid trata dos sintaxis de forma radicalmente distinta:
// DELEGADO: nombre en camelCase. Se registra en el sistema de delegación.<button onClick={handleClick}>Guardar</button>// NATIVO: prefijo on: con el nombre exacto. Es addEventListener directo.<button on:click={handleClick}>Guardar</button>Con onClick, Solid no llama a addEventListener sobre el botón. En su lugar, guarda tu handler en una propiedad interna del nodo (button.$$click = handleClick) y se asegura de que exista un único listener de click colgado del document. Con on:click, el nombre se respeta tal cual (es sensible a mayúsculas) y se hace button.addEventListener("click", handleClick) sobre ese nodo y solo ese nodo.
Conceptualmente, el compilador emite dos cosas bien distintas:
// onClick delegado: guarda el handler y registra el tipo en el document.button.$$click = handleClick;delegateEvents(["click"]); // idempotente: un unico listener global por tipo// on:click nativo: addEventListener directo sobre este nodo y solo este.button.addEventListener("click", handleClick);La lista de eventos que Solid delega es curada y finita: click, input, keydown, keyup, pointerdown, pointerup, mousedown, mouseup, touchstart y unos cuantos más, todos ellos eventos que burbujean. Si escribes onMouseEnter (un evento que no burbujea y no está en la lista), Solid no puede delegarlo: cae al camino nativo y hace un addEventListener normal por ti. Es decir, onXxx es “delega si puedes, si no adjunta nativo”, mientras que on:xxx es “siempre nativo, sin excepciones”.
Delegación: un listener para gobernarlos a todos
La delegación explota que los eventos burbujean del target hacia arriba. En lugar de mil listeners de click (uno por fila de una tabla), hay uno solo en el document. Cuando disparas un click, el evento burbujea hasta el document, allí el único listener de Solid lo captura y simula la propagación: recorre desde event.target hacia arriba, y en cada nodo que tenga un $$click guardado, lo invoca fijando currentTarget a ese nodo. Respeta stopPropagation mirando una bandera interna mientras asciende.
Las ventajas son de escala. La memoria no crece con el número de elementos: añadir diez mil filas no añade diez mil listeners. Y como el listener vive en el document, funciona con nodos que aparecen después: no hay que re-enganchar nada al insertar. Por eso el patrón de listas en Solid casi nunca necesita pensar en el coste de los handlers.
Ese mismo diseño esconde una trampa de orden que separa a quien sabe de quien copia. Como el listener delegado vive en el document, se dispara después de que el evento haya burbujeado por todo el árbol. Cualquier addEventListener nativo colgado de un ancestro intermedio se ejecuta antes que tu handler delegado, porque está más cerca del target en el camino de burbujeo. Y si ese listener intermedio llama a event.stopPropagation(), el evento nunca alcanza el document: tu onClick delegado no se dispara jamás.
Datos en el handler sin closures
El instinto de React es capturar datos en una closure: onClick={() => remove(item.id)}. Funciona, pero crea una función nueva por cada elemento de la lista. Solid ofrece una forma de tupla que evita esa asignación:
function Fila(props: { id: number; nombre: string }) { // remove recibe el dato como PRIMER argumento y el evento como segundo. return <li onClick={[remove, props.id]}>{props.nombre}</li>;}function remove(id: number, e: MouseEvent) { // e.currentTarget es el <li>; id llega ya vinculado, sin closure. console.log("borrar", id, e.currentTarget);}Cuando el valor de un binding de evento es un array [fn, dato], Solid llama a fn(dato, evento). La misma referencia de función remove se reutiliza en las mil filas; lo único que cambia es el dato vinculado, que Solid guarda aparte. Más allá del micro-ahorro de asignaciones, el patrón separa la lógica (una función pura y testeable) de la plantilla. Un ejemplo completo lo reúne todo —delegación, tupla y classList reactivo— en un componente idiomático:
import { createSignal, For } from "solid-js";function ListaSeleccionable(props: { items: { id: number; texto: string }[] }) { const [sel, setSel] = createSignal<number | null>(null); // Handler tipado y reutilizado por todas las filas: el dato primero. const elegir = (id: number, e: MouseEvent) => { setSel(id); (e.currentTarget as HTMLLIElement).scrollIntoView({ block: "nearest" }); }; return ( <ul> <For each={props.items}> {(it) => ( <li onClick={[elegir, it.id]} classList={{ sel: sel() === it.id }}> {it.texto} </li> )} </For> </ul> );}El binding nativo, por su parte, abre la puerta a lo que la delegación no puede dar:
// Fase de captura: corre antes que los handlers de los hijos.<div oncapture:click={enCaptura}>zona</div>// Evento personalizado con mayusculas que emite un web component.<mi-widget on:MiEvento={alCustom} />// Opciones del listener nativo: passive, once, capture.<div on:wheel={{ passive: true, handleEvent: alScroll }} />