onCleanup: registrar la limpieza
onCleanup empuja una función de limpieza a la lista del owner actual, y Solid la dispara en dos momentos: justo antes de que la computación se re-ejecute y cuando su dueño se dispone. El teardown simétrico del setup.
Si el árbol de ownership es el esqueleto, onCleanup es el músculo que lo mueve. Es la primitiva con la que declaras el reverso de cualquier efecto: por cada recurso que abres —un temporizador, un listener, un socket, una suscripción—, registras la operación que lo cierra. No es un apéndice opcional que pones “por si acaso”: es la mitad simétrica de lo que un efecto significa. Solid la dispara en dos momentos precisos, y saber cuáles son te da control total sobre el ciclo de vida de todo lo que tu código toca fuera del grafo.
- Saber qué hace
onCleanup: empujar una función a las limpiezas del owner actual. - Dominar sus dos momentos de disparo: antes de cada re-ejecución y al disponer el owner.
- Reconocer los casos típicos: intervalos, listeners, observadores, sockets y peticiones.
- Entender por qué debe registrarse de forma síncrona dentro de un owner.
Qué registra y dónde
onCleanup(fn) toma una función y la añade a la lista de limpiezas del owner activo en ese instante. Eso es todo lo que hace al llamarla: no ejecuta nada, solo agenda. La ejecución llega después, cuando el owner lo decide. Puedes llamarla en cualquier punto de un owner —en el cuerpo de un componente, dentro de un efecto, incluso dentro de una función auxiliar— siempre que sea de forma síncrona, porque lee la variable del owner actual, que solo está fijada durante la ejecución síncrona.
import { createEffect, onCleanup } from "solid-js";
createEffect(() => {
const id = setInterval(() => tick(), 1000);
onCleanup(() => clearInterval(id)); // se agenda en el owner del efecto
});
Puedes registrar varias limpiezas en el mismo owner —una por recurso— y todas se ejecutarán al llegar el momento, en orden inverso al de registro. La regla de oro es la proximidad: escribe cada onCleanup pegado a la línea que abre el recurso, para que apertura y cierre se lean juntos y ninguno se olvide.
Los dos momentos
Solid dispara las limpiezas de un owner en dos situaciones, y conviene tenerlas nítidas:
flowchart LR R1[run 1 abre recurso] --> D[cambia una dependencia] D --> C1[cleanup del run 1] C1 --> R2[run 2 abre recurso nuevo] R2 --> X[se dispone el owner] X --> C2[cleanup del run 2] style R1 fill:#a6e3a1,color:#11111b style R2 fill:#a6e3a1,color:#11111b style C1 fill:#f38ba8,color:#11111b style C2 fill:#f38ba8,color:#11111b
El primer momento es la re-ejecución. Cuando una computación que se repite —un efecto o un memo— va a correr de nuevo porque cambió una de sus dependencias, Solid ejecuta antes las limpiezas registradas en la corrida anterior. Así, si el efecto abrió un socket para el canal a y ahora el canal pasa a b, primero se cierra el socket de a y solo entonces el cuerpo abre el de b. Nunca hay dos abiertos ni uno huérfano.
El segundo momento es la disposición. Cuando el owner se destruye —porque el componente sale del DOM o porque se llamó a dispose—, se ejecuta la última tanda de limpiezas pendientes. Es el desmontaje final: lo que quedara abierto en la última corrida se cierra aquí.
createEffect(() => {
const canal = suscripcion();
const socket = abrir(canal);
onCleanup(() => socket.cerrar()); // corre antes de cada rerun Y al disponer
});
En un onMount —un efecto que corre una sola vez y no rastrea nada— solo existe el segundo momento: la limpieza que registres dentro correrá únicamente al desmontar, porque nunca hay una segunda corrida.
Dónde se engancha según dónde lo llames
onCleanup siempre se engancha al owner activo, y ese owner cambia según el lugar de la llamada. Si lo pones en el cuerpo del componente, se engancha al owner del componente, que solo se dispone una vez, al desmontar; por eso ahí la limpieza corre una sola vez. Si lo pones dentro de un efecto que se repite, se engancha al owner del efecto, que se re-crea en cada corrida; por eso ahí la limpieza corre antes de cada rerun y también al final.
import { createEffect, onCleanup } from "solid-js";
function Widget() {
// owner del componente: corre solo al desmontar
onCleanup(() => console.log("adios Widget"));
createEffect(() => {
const id = abrir(canal());
// owner del efecto: corre en cada cambio de canal y al desmontar
onCleanup(() => cerrar(id));
});
return <div />;
}
La misma primitiva significa “al desmontar” en el cuerpo y “antes de cada rerun” dentro de un efecto, sin cambiar de nombre, porque el cuándo no lo decide onCleanup: lo decide el owner al que se engancha. Es la consecuencia directa de que cada computación sea su propio owner.
Una consecuencia práctica y potente: puedes envolver un addEventListener con su onCleanup dentro de una función auxiliar reutilizable —una primitiva propia— y llamarla desde cualquier componente. La limpieza se enganchará al owner de quien la invoque, sin que la función necesite saber nada de él.
import { onCleanup } from "solid-js";
// primitiva reutilizable: se engancha al owner de quien la llame
function usarEvento(target, tipo, handler) {
target.addEventListener(tipo, handler);
onCleanup(() => target.removeEventListener(tipo, handler));
}
Que onCleanup lea el owner del contexto de llamada es lo que hace componibles a los custom primitives de Solid. Una función como usarEvento, usarIntervalo o usarMedia encapsula la pareja abrir/cerrar y se reutiliza en decenas de componentes: cada uno hereda la limpieza sin escribir una sola línea de teardown. Es el equivalente reactivo de un objeto con destructor, pero atado al árbol de owners en vez de a un bloque try/finally.
Los casos típicos
Casi todo lo que “vive” fuera del grafo reactivo necesita una limpieza. Estos son los sospechosos habituales:
Temporizadores
setInterval y setTimeout se cierran con clearInterval y clearTimeout.
Listeners del DOM
Cada addEventListener exige su removeEventListener con la misma referencia.
Observadores
ResizeObserver, IntersectionObserver y MutationObserver se apagan con disconnect.
Red y suscripciones
Un WebSocket se cierra con close; una petición se aborta con AbortController.
El patrón de una petición cancelable ilustra la simetría perfecta entre abrir y cerrar:
import { createEffect, onCleanup } from "solid-js";
createEffect(() => {
const ctrl = new AbortController();
fetch(`/api/${id()}`, { signal: ctrl.signal })
.then((r) => r.json())
.then(aplicar)
.catch(ignorarAbortadas);
onCleanup(() => ctrl.abort()); // cancela la peticion vieja al cambiar id
});
Al cambiar id, la limpieza aborta la petición en curso antes de lanzar la nueva: no llegan respuestas fuera de orden ni se aplica una respuesta obsoleta. Lo mismo vale para librerías externas —un gráfico, un mapa, un editor—: si su constructor reserva algo, su método destroy o dispose va dentro de un onCleanup.
onCleanup necesita un owner activo para agendar la limpieza. Si lo llamas donde no hay ninguno —en el nivel de módulo, dentro de un setTimeout, o después de un await—, no falla con estruendo: simplemente no agenda nada, y en modo desarrollo Solid avisa por consola. Ese silencio es una fuente clásica de fugas. Cuando necesites registrar limpieza tras un salto asíncrono, tendrás que recuperar el owner a mano; es justo lo que resuelve runWithOwner en la próxima lección.
Es tentador ver onCleanup como higiene —lo que añades al final para no dejar cabos sueltos—, pero conceptualmente es la mitad exacta de lo que declara un efecto. Un efecto no describe una acción puntual: describe una correspondencia sostenida entre un estado reactivo y algo del mundo exterior. “Mientras el canal valga a, debe existir un socket a a” es una relación que hay que mantener en el tiempo, y mantener una relación no es solo establecerla cuando el estado aparece, sino deshacerla cuando el estado cambia o desaparece. Si solo abres y nunca cierras, no estás sosteniendo una correspondencia: estás acumulando basura. Por eso Solid ata la limpieza al mismo ciclo que la ejecución y al mismo árbol de ownership que gobierna la vida de todo lo demás; la creación y la destrucción de un recurso son el mismo hecho reactivo visto desde sus dos extremos. Cuando escribas un efecto que toca el exterior, no te preguntes “qué hago cuando esto cambia”, sino “qué relación quiero sostener, y cómo se establece y se rompe”. El cuerpo la establece; el onCleanup la rompe. Con esa pareja, la gestión de recursos deja de ser una lista de tareas que recordar y se vuelve una propiedad del código: cada efecto limpia exactamente lo que ensucia.
- Monta un
setIntervaldentro de un efecto que dependa de un signal; cambia el signal y confirma que el intervalo viejo muere antes de nacer el nuevo. - Añade un listener de
resizecon suremoveEventListeneren unonCleanup; desmonta el componente y verifica en las devtools que el listener desaparece. - Escribe una petición con
AbortControllerque se cancele al cambiar su dependencia; provoca cambios rápidos y comprueba que no se aplica ninguna respuesta obsoleta. - Registra dos
onCleanupdistintos en un mismo efecto y anota el orden en que corren. - Llama a
onCleanupdentro de unsetTimeouty observa el aviso de consola: acabas de crear una limpieza huérfana.