Saltar al contenido

El callback dispose: disponer a mano

Qué hace exactamente dispose —recorrer el subárbol de owners y ejecutar cleanups de abajo hacia arriba, de forma idempotente— y cuándo la disposición manual es inevitable: efectos globales fuera del árbol, tests deterministas y herramientas. El puente para atar una raíz manual a un owner existente con onCleanup.

Qué hace dispose por dentro

dispose no es un simple apagar. Cuando lo invocas, Solid recorre el subárbol de owners que cuelga de esa raíz y, en cada nodo, ejecuta sus onCleanup registrados y desconecta sus computaciones de las señales que observaban. El recorrido va de abajo hacia arriba: primero mueren los hijos, luego los padres, para que ninguna limpieza superior corra cuando sus dependientes ya no existen. Al terminar, la raíz queda vacía —sin hijos, sin suscripciones— y las señales que sobrevivan dejan de tener a quién notificar.

El orden importa más de lo que parece. Imagina un efecto padre que abrió una conexión y un efecto hijo que la usa. Si la limpieza del padre corriera primero, cerraría la conexión mientras el hijo todavía cree tenerla, y la limpieza del hijo operaría sobre un recurso ya muerto. Recorrer de hijos a padres elimina esa clase entera de errores: cuando se limpia un nodo, todo lo que dependía de él ya se limpió. Es la misma garantía del desmontaje de componentes, y por la misma razón: es el mismo recorrido sobre el mismo árbol.

TSX
import { createRoot, createEffect, onCleanup } from "solid-js";const dispose = createRoot((d) => {  createEffect(() => {    const id = setInterval(() => console.log("tick"), 1000);    onCleanup(() => clearInterval(id)); // se ejecutara al disponer  });  return d;});// ...mas tarde, cuando ya no lo necesitas:dispose(); // corre el onCleanup: el intervalo se detiene, el efecto muere

Dos propiedades importan. Es idempotente: llamar a dispose dos veces no rompe nada, la segunda encuentra un árbol ya limpio y no hace trabajo. Y es síncrono: al volver de dispose, la limpieza ya ocurrió; no hay cola diferida ni microtarea que esperar. Esto lo hace ideal para tests, donde necesitas certeza inmediata de que el ámbito murió.

Una advertencia sobre errores: tus funciones de onCleanup no deberían lanzar. La limpieza es la fase en la que sueltas recursos, no en la que validas nada, y una excepción a mitad del recorrido puede dejar sin ejecutar las limpiezas que faltaban. Mantén cada una pequeña, independiente y a prueba de fallos —envuelve en try lo que de verdad pueda reventar—, de modo que cerrar un recurso jamás impida cerrar el siguiente.

Cuándo hace falta disponer a mano

La regla es simple: si el owner no lo creó un componente ni render, la limpieza es tuya. Tres escenarios lo exigen.

El patrón de test es el más ilustrativo, porque comprime todo el ciclo en pocas líneas: abrir el ámbito, ejercitar, aseverar, cerrar.

TSX
import { createRoot, createSignal, createMemo } from "solid-js";test("el memo deriva y se recalcula", () => {  createRoot((dispose) => {    const [precio, setPrecio] = createSignal(100);    const conIva = createMemo(() => precio() * 1.21);    expect(conIva()).toBeCloseTo(121);    setPrecio(200);    expect(conIva()).toBeCloseTo(242);    dispose(); // el ambito muere con el test; nada se filtra al siguiente  });});

Fíjate en lo que este patrón compra en un test: aislamiento. Sin createRoot, las señales y efectos que crea un caso quedarían bajo el owner nulo global, vivos y suscritos cuando arranca el siguiente; un efecto del test A podría dispararse por un cambio del test B y ensuciar aserciones de formas casi imposibles de rastrear. La raíz por test convierte cada caso en un universo desechable: nace vacío, vive lo que dura la función y se desintegra por completo en el dispose. Los helpers de testing de Solid envuelven esto en una utilidad, pero por debajo es siempre esta misma raíz manual con disposición garantizada.

Y no solo en tests. La misma raíz sirve para herramientas efímeras: evaluar una derivación reactiva una sola vez, sin dejar el memo suscrito. Abres, lees y dispones en el acto.

TSX
import { createRoot, createMemo } from "solid-js";// Evaluar una derivacion una vez y desmontarla al instante.function evaluarUnaVez<T>(fn: () => T): T {  return createRoot((dispose) => {    const valor = createMemo(fn)();    dispose(); // el memo no sobrevive a la lectura    return valor;  });}

El puente: onCleanup(dispose)

A veces abres una raíz manual pero existe un owner alrededor —estás dentro de un componente, o de otra raíz— y no quieres acordarte de disponer a mano. La solución idiomática es registrar el dispose de la raíz hija en la limpieza del owner ambiente: así, cuando el owner de fuera muera, arrastrará consigo a la raíz que abriste.

TSX
import { createRoot, onCleanup } from "solid-js";function montarWidgetReactivo() {  // Dentro de un componente: hay un owner activo alrededor.  const dispose = createRoot((d) => {    // ...efectos y memos del widget...    return d;  });  // Ata la vida de la raiz manual al owner de este componente:  onCleanup(dispose);}

Esta es la costura entre lo automático y lo manual. createRoot te dio una vida independiente; onCleanup(dispose) vuelve a coserla al árbol cuando te conviene que herede una muerte ajena. Sin la costura, la raíz sobrevive a su componente y tienes una fuga; con ella, recuperas la comodidad del desmontaje automático sin renunciar a haber fabricado el scope tú.

¿Y si no hay owner ambiente donde coserlo —estás en un módulo, en un setTimeout, lejos de todo componente—? Entonces no hay a quién delegar la muerte, y la disposición vuelve a ser enteramente tuya: guarda el dispose, exponlo y llámalo desde el evento que marque el fin de la tarea. El nivel siguiente muestra cómo getOwner y runWithOwner permiten además capturar un owner en un punto y adscribirle computaciones desde otro; por ahora basta la regla: si hay owner alrededor, cose con onCleanup; si no lo hay, la llave del dispose no la suelta nadie más que tú.