Integrar librerías imperativas
El patrón para envolver charts, mapas y editores imperativos en un componente Solid: onMount crea la instancia cuando el DOM ya existe, onCleanup la destruye al desmontar, y un createEffect por cada prop sincroniza los cambios mutando la instancia en vez de recrearla. La instancia se guarda en una variable normal, no en un signal, y cada efecto empuja al mundo imperativo solo lo que cambió.
Antes o después toca casar Solid con una librería que no sabe nada de reactividad: un gráfico, un mapa, un editor de código, un reproductor de vídeo. Todas exponen una API imperativa —creas una instancia sobre un nodo, la mutas con métodos, la destruyes al final— que vive en un mundo distinto al del grafo reactivo. El puente entre ambos mundos es un patrón de tres tiempos que ya conoces pieza por pieza: onMount para instanciar cuando el DOM existe, onCleanup para destruir al desmontar, y un createEffect por cada prop para sincronizar los cambios sin recrear nada.
- Instanciar una librería imperativa dentro de
onMount, cuando el nodo ya está en el documento. - Destruir la instancia con
onCleanuppara no filtrar memoria ni listeners. - Sincronizar cada prop reactiva con su propio
createEffectque muta la instancia. - Entender por qué la instancia se guarda en una variable normal y no en un signal.
El patrón de tres tiempos
La librería imperativa tiene su propio ciclo: nace, cambia, muere. Tu trabajo es mapear ese ciclo a las primitivas de Solid sin dejar cabos sueltos. El montaje crea, los efectos sincronizan, la limpieza destruye.
flowchart TD A[onMount cuando el DOM existe] --> B[crear la instancia sobre el nodo] B --> C[un efecto por cada prop que sincronizar] C --> D[cada efecto muta la instancia al cambiar la prop] B --> E[onCleanup destruye la instancia al desmontar] style A fill:#89b4fa,color:#11111b style D fill:#a6e3a1,color:#11111b style E fill:#f38ba8,color:#11111b
Lo importante es que la instancia se crea una sola vez y luego se muta, nunca se recrea en cada cambio de prop. Recrear un gráfico o un mapa en cada actualización sería caro y borraría su estado interno —zoom, selección, scroll—. El grano fino de Solid encaja perfecto aquí: cada prop tiene su efecto, y cada efecto empuja al mundo imperativo exactamente el cambio que le corresponde.
onMount instancia, onCleanup destruye
Toma un gráfico como caso concreto. La librería expone un constructor que recibe el nodo canvas y una configuración, métodos para actualizar, y un destroy. La instancia se guarda en una variable local del componente:
import { onMount, onCleanup } from "solid-js";
import { Chart } from "chart.js/auto";
function Grafico(props: { datos: number[]; leyenda: boolean }) {
let lienzo!: HTMLCanvasElement;
let grafico: Chart | undefined;
onMount(() => {
grafico = new Chart(lienzo, {
type: "line",
data: { labels: [], datasets: [{ data: props.datos }] },
options: { plugins: { legend: { display: props.leyenda } } },
});
onCleanup(() => grafico?.destroy()); // libera canvas, listeners y animaciones
});
return <canvas ref={lienzo} />;
}
onMount es obligatorio, no opcional: la librería necesita un nodo insertado para medirlo y dibujar, y en el cuerpo del componente ese nodo aún no existe. onCleanup es igual de obligatorio: sin destroy, cada montaje deja atrás un canvas vivo, animaciones corriendo y listeners globales colgados. La pareja mantiene la correspondencia “mientras el componente vive, existe el gráfico”.
Un efecto por cada prop
La instancia ya existe; falta que reaccione a las props. Aquí está la parte que distingue a Solid: en vez de un único bloque que reconstruye todo cuando “algo” cambia, escribes un efecto por cada prop independiente, y cada uno muta solo su parte. Los declaras dentro de onMount, después de crear la instancia, para que solo arranquen cuando ya hay algo que mutar.
import { onMount, onCleanup, createEffect } from "solid-js";
import { Chart } from "chart.js/auto";
function Grafico(props: { datos: number[]; leyenda: boolean }) {
let lienzo!: HTMLCanvasElement;
let grafico: Chart | undefined;
onMount(() => {
grafico = new Chart(lienzo, {
type: "line",
data: { labels: [], datasets: [{ data: [] }] },
options: { plugins: { legend: { display: props.leyenda } } },
});
createEffect(() => {
grafico!.data.datasets[0].data = props.datos; // se re-ejecuta al cambiar datos
grafico!.update();
});
createEffect(() => {
grafico!.options.plugins!.legend!.display = props.leyenda; // solo la leyenda
grafico!.update();
});
onCleanup(() => grafico?.destroy());
});
return <canvas ref={lienzo} />;
}
Cada efecto rastrea únicamente la prop que lee: el primero se re-ejecuta cuando cambia props.datos y no le importa la leyenda; el segundo, al revés. Cambiar los datos no reinicia la leyenda ni viceversa, porque son suscripciones distintas. Esa separación es gratis en Solid y es justo lo que una librería imperativa necesita: mutaciones mínimas y dirigidas.
Un efecto creado dentro de onMount rastrea con normalidad —el untrack de onMount solo afecta a las lecturas síncronas de su cuerpo, no a los cómputos anidados— y pertenece al dueño del componente, así que su limpieza también corre al desmontar.
La instancia del gráfico se crea una vez y no la lee el JSX ni ningún cómputo reactivo: solo la mutas desde los efectos. Por eso va en un let normal, no en un signal. Meterla en un signal no aportaría nada —nadie necesita reaccionar a “la instancia cambió”, porque no cambia— y añadiría ruido. Reserva los signals para el estado que la interfaz observa; la instancia imperativa es un recurso, no estado reactivo.
Dos trampas frecuentes. Primera: si en el constructor lees props.datos pero luego nunca creas un efecto que lo vuelva a leer, capturas el valor inicial y jamás se actualiza; toda prop que deba sincronizarse necesita su efecto. Segunda: como todo esto ocurre en onMount, no corre en el servidor, lo cual es correcto —estas librerías tocan el DOM—, pero significa que en SSR el componente renderiza solo el canvas vacío hasta que hidrata en el cliente. Planifícalo si te importa el primer pintado.
Cuando la sincronización de una prop es cara —redibujar miles de puntos— acota el efecto con on para controlar con exactitud qué lo dispara y no leer dentro más signals de los necesarios:
createEffect(on(() => props.datos, (datos) => {
grafico!.data.datasets[0].data = datos;
grafico!.update("none"); // sin animacion para grandes volumenes
}));
El patrón completo, de un vistazo:
Montar
onMount crea la instancia sobre el nodo capturado por ref, en el único momento en que el DOM ya existe.
Sincronizar
Un createEffect por prop muta la instancia cuando esa prop cambia, sin recrearla ni perder su estado interno.
Destruir
onCleanup llama al destroy de la librería al desmontar, devolviendo canvas, listeners y animaciones.
Envolver una librería imperativa es el examen final del nivel, porque obliga a usar las tres piezas —onMount, onCleanup y los efectos— como lo que realmente son: los puntos de contacto entre el grafo reactivo de Solid y un sistema que no sabe reaccionar. Piensa en el componente como una membrana. Hacia adentro, del lado imperativo, hay una instancia con estado propio que solo entiende llamadas a métodos; hacia afuera, del lado de Solid, hay props reactivas que cambian solas. onMount es el punto donde la membrana da a luz la instancia, en el único instante en que el DOM ya existe. Los efectos son los poros de la membrana: cada uno deja pasar el cambio de una prop y lo traduce a una mutación, de forma que el flujo va siempre de reactivo a imperativo, dirigido y mínimo. onCleanup es donde la membrana se disuelve y devuelve al sistema operativo todo lo que la instancia había tomado. La razón de que este patrón sea tan robusto es que no intenta hacer reactiva a la librería —eso sería pelear con su naturaleza—, sino colocar cada traducción en el lugar exacto del ciclo de vida: crear cuando hay nodo, sincronizar cuando cambia el dato, destruir cuando muere el dueño. Cuando interiorizas que un componente puede ser una membrana y no solo un trozo de interfaz, integrar cualquier librería del ecosistema —mapas, editores, reproductores, físicas— deja de ser un caso especial y se vuelve el mismo patrón de tres tiempos aplicado una vez más.
- Envuelve una librería de gráficos en un componente: crea la instancia en
onMountsobre uncanvascapturado porrefy destrúyela enonCleanup. - Añade un
createEffectque sincronice los datos del gráfico cuando cambieprops.datos, mutando la instancia en vez de recrearla. - Añade un segundo efecto para otra prop (color, leyenda, escala) y confirma que cambiar una no reinicia la otra.
- Desmonta el componente y verifica que la instancia se destruyó y no quedan listeners ni animaciones vivas.
- Explica por qué la instancia va en un
lety no en un signal, y por qué los efectos se declaran dentro deonMount.