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

onCleanup: liberar al desmontar

onCleanup en componentes: la mitad simétrica de onMount que libera intervalos, listeners y suscripciones cuando el dueño se destruye. Cómo se ata al owner, por qué es más flexible que un onUnmount, y el orden exacto de la limpieza: los hijos antes que los padres en el árbol, y dentro de un mismo dueño en orden inverso al registro (LIFO).

⏱ 13 min

Todo lo que un componente reserva del mundo real —un intervalo, un listener global, un socket, una suscripción a un store externo— debe soltarse cuando el componente desaparece, o tendrás una fuga silenciosa que se acumula desmontaje tras desmontaje. Solid no ofrece un onUnmount suelto, sino algo más preciso y más general: onCleanup, que ata una función de teardown al dueño actual y la ejecuta cuando ese dueño muere. Es la mitad simétrica de onMount, y entender su orden de disparo es entender cómo Solid garantiza que nada sobreviva a quien lo creó.

🎯 Al terminar esta lección sabrás
  • Registrar limpieza con onCleanup para soltar recursos al desmontar el componente.
  • Ver la simetría entre onMount (arranque) y onCleanup (cierre) como un par.
  • Comprender que onCleanup se ata al dueño, no a un evento del navegador.
  • Dominar el orden de la limpieza: hijos antes que padres, y dentro de un dueño en orden inverso (LIFO).

Liberar lo que reservaste

El patrón canónico empareja adquisición y liberación en el mismo bloque. Lo que abres en onMount, lo cierras en un onCleanup inmediato, de modo que ambas mitades se leen juntas y ninguna se olvida.

import { onMount, onCleanup } from "solid-js";

function Reloj() {
  let etiqueta!: HTMLTimeElement;
  onMount(() => {
    const id = setInterval(() => {
      etiqueta.textContent = new Date().toLocaleTimeString();
    }, 1000);
    onCleanup(() => clearInterval(id));   // corre al desmontar
  });
  return <time ref={etiqueta} />;
}

Cuando el componente se desmonta, Solid ejecuta esa función y el intervalo se detiene. No hay que recordar hacerlo desde fuera ni orquestar ningún desmontaje manual: la liberación viaja pegada a la reserva.

onCleanup no está limitado a onMount. Puede invocarse en cualquier punto de un dueño reactivo —en el cuerpo del componente, dentro de un createEffect, dentro de otro onCleanup— y siempre queda registrado en el dueño vigente. En el cuerpo del componente, ese dueño es el componente entero, así que la limpieza corre al desmontar.

function Documento() {
  const alPresionar = (e: KeyboardEvent) => {
    if (e.key === "Escape") cerrar();
  };
  document.addEventListener("keydown", alPresionar);
  onCleanup(() => document.removeEventListener("keydown", alPresionar)); // en el cuerpo, se limpia al desmontar
  return <dialog>...</dialog>;
}

La simetría con onMount

onMount y onCleanup forman una pareja: uno establece una relación con el mundo, el otro la deshace. No son dos hooks independientes que casualmente se complementan, sino los dos extremos de un mismo hecho. Mientras el componente vive, existe el intervalo; cuando muere, no debe existir. Esa correspondencia es lo que la pareja mantiene.

La diferencia con un onUnmount al estilo clásico es que onCleanup no depende de estar en un único hook de cierre. Como se ata al dueño, puedes registrar la limpieza exactamente donde reservaste el recurso, por muchos que sean y por dispersos que estén. Cada adquisición lleva su liberación al lado, en lugar de acumularse todas en una función de desmontaje lejana que hay que mantener sincronizada a mano.

💡
onCleanup dentro de un efecto también corre entre reruns

En el cuerpo o en onMount, el onCleanup se dispara una vez, al desmontar. Pero dentro de un createEffect el mismo onCleanup cumple doble función: corre antes de cada nueva ejecución del efecto y también al destruirse el dueño. Es el mismo mecanismo; cambia el dueño al que se engancha. Por eso un listener que dependa de un signal se pone en un efecto, no en onMount: así se recicla en cada cambio además de limpiarse al final.

El orden de la limpieza

Cuando un dueño se destruye, Solid no ejecuta las limpiezas en un orden arbitrario: sigue una disciplina precisa y determinista, en dos ejes.

En el árbol de dueños, la limpieza es de abajo hacia arriba: los hijos se disponen antes que su padre. Solid recorre primero el subárbol de dueños hijos —recursivamente— y solo después ejecuta las limpiezas propias del dueño. Un componente anidado se desmonta por completo antes de que corra el onCleanup del componente que lo contiene.

Dentro de un mismo dueño, las limpiezas corren en orden inverso al de su registro: la última que registraste es la primera en ejecutarse. Es la disciplina de una pila, y tiene sentido físico: lo último que se abrió suele ser lo primero que hay que cerrar, porque puede depender de lo que se abrió antes.

flowchart TD
P[el dueno del componente se destruye] --> H[primero se disponen los hijos]
H --> HR[cada hijo en orden inverso a su creacion]
HR --> C[luego corren los cleanups propios]
C --> L[en orden inverso al registro LIFO]
style P fill:#89b4fa,color:#11111b
style C fill:#f38ba8,color:#11111b
style L fill:#f38ba8,color:#11111b

En la práctica rara vez tienes que pensar en el orden exacto si cada recurso es independiente. Importa cuando hay dependencias entre recursos: si el recurso B se construyó a partir del A, registra B después que A y el orden inverso garantiza que B se cierre antes de que A desaparezca bajo sus pies.

function Sesion() {
  onMount(() => {
    const conexion = abrirConexion();
    onCleanup(() => conexion.cerrar());        // se registra primero -> se limpia despues

    const canal = conexion.abrirCanal();       // depende de la conexion
    onCleanup(() => canal.desuscribir());      // se registra despues -> se limpia primero
  });
  return <div>...</div>;
}

Qué se libera y desde dónde

Casi todo lo que hay que liberar cae en cuatro familias, y todas siguen el mismo par de abrir y registrar. Un caso muy frecuente más allá del intervalo es suscribirse a una API del navegador o a un store externo:

function Tema() {
  const consulta = window.matchMedia("(prefers-color-scheme: dark)");
  const alCambiar = (e: MediaQueryListEvent) => aplicarTema(e.matches);
  consulta.addEventListener("change", alCambiar);
  onCleanup(() => consulta.removeEventListener("change", alCambiar));
  return <div>...</div>;
}

El recurso vive fuera de Solid —lo posee el navegador—, pero su suscripción queda atada al dueño del componente y se retira sola al desmontar. Ese es el valor de onCleanup: convierte cualquier recurso externo en algo con el ciclo de vida del componente.

⏱️

Temporizadores

setInterval y setTimeout: guarda el id y llama a clearInterval o clearTimeout en la limpieza.

👂

Listeners

addEventListener sobre window, document o un nodo externo: retíralo con removeEventListener.

📡

Suscripciones

Sockets, stores externos, matchMedia: desuscribe o cierra la conexión en el onCleanup.

🔭

Observadores

ResizeObserver, IntersectionObserver y MutationObserver: llama a disconnect al desmontar.

La disposición es una propiedad del árbol, no una tarea tuya

El error mental que hay que abandonar es imaginar la limpieza como una lista de pendientes que tú debes acordarte de ejecutar al final. En Solid la disposición es una propiedad estructural del árbol de ownership, tan automática y ordenada como la destrucción de variables locales al salir de una función. Cada efecto, cada componente, cada createRoot es un dueño; cada dueño conoce a sus hijos y sus limpiezas; y cuando muere, recorre ese subárbol de abajo arriba ejecutando teardown en orden inverso, sin que tú orquestes nada. Esto convierte la gestión de recursos de una obligación que se olvida a las tres de la mañana en una garantía que el runtime cumple por construcción: es imposible que un hijo sobreviva a su padre, imposible que un recurso quede huérfano si registraste su onCleanup en el dueño correcto. La única responsabilidad que te queda —y es real— es no crear cómputos fuera de todo dueño, porque entonces no hay árbol que los limpie. Ahí es donde entra createRoot, que crea un dueño explícito y te devuelve su disposición para que tú decidas cuándo cortar. Todo lo demás se limpia solo, en el orden correcto, porque el orden vive en la forma del árbol y no en tu memoria.

⚔️ Cierra todo lo que abres, en el orden correcto
  1. Monta un setInterval en onMount y libéralo con onCleanup; desmonta el componente y confirma en la consola que el intervalo se detuvo.
  2. Añade un listener global de keydown en el cuerpo del componente con su onCleanup y verifica que se retira al desmontar.
  3. Registra dos onCleanup en el mismo dueño con un console.log cada uno y observa que se ejecutan en orden inverso al registro.
  4. Anida dos componentes, cada uno con su onCleanup, y comprueba que el hijo limpia antes que el padre.
  5. Explica en una frase por qué un recurso derivado de otro debe registrar su limpieza después que el recurso base.