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.
En los componentes nunca dispones nada: montas, el framework desmonta, y las limpiezas corren solas. Esa automatización es tan cómoda que oculta la otra mitad del sistema. dispose es esa mitad hecha explícita: la función que recibe todo createRoot y que, al invocarse, ejecuta en un instante la ceremonia de muerte que un componente hace al desmontarse. Hay tres territorios donde el árbol no dispone por ti —efectos que viven fuera de él, tests que crean y descartan reactividad, herramientas que inspeccionan el grafo— y en los tres, olvidar el dispose es olvidar cerrar la llave del gas.
- Describir qué hace
dispose: recorrer el subárbol de owners y correr sus limpiezas. - Reconocer los tres casos donde la disposición manual es inevitable.
- Escribir tests deterministas que crean, ejercitan y disponen un ámbito reactivo.
- Atar la vida de una raíz manual a un owner existente con
onCleanup(dispose).
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.
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.
Efectos fuera del árbol
Un efecto que arrancas en un setTimeout, en un handler global o a nivel de módulo no tiene componente que lo desmonte. Envuélvelo en createRoot y guarda su dispose para el día que ese efecto deba parar.
Tests deterministas
Cada test que ejercita reactividad crea señales y efectos; sin disponer al final, se acumulan entre casos y contaminan al siguiente. createRoot por test, dispose en el teardown.
Herramientas efímeras
Medir un grafo, evaluar una derivación una sola vez, construir un inspector: computaciones que nacen para una tarea puntual y deben desaparecer sin dejar rastro reactivo.
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.
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.
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.
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ú.
flowchart TD A[dispose invocado] --> B[Recorre el subarbol de owners] B --> C[Hijos primero: corren sus onCleanup] C --> D[Luego los padres del subarbol] D --> E[Desconecta computaciones de las senales] E --> F[Raiz vacia: sin hijos ni suscripciones] style A fill:#89b4fa,color:#11111b style C fill:#f38ba8,color:#11111b style F fill:#a6e3a1,color:#11111b
El dispose está disponible ya dentro del callback, no solo fuera. Eso permite ámbitos que se cierran a sí mismos cuando se cumple una condición: un efecto que espera un valor y, al llegar, llama a dispose para desmontarse. Es el equivalente reactivo de un one-shot: vive lo justo para observar lo que le importa y se suicida limpiamente. Cuidado, eso sí, con disponer en mitad de la primera ejecución del propio efecto que dispara la disposición; deja que el efecto termine su corrida y dispón después.
createRoot acepta un segundo parámetro, un owner «desconectado», que reemplaza al owner ambiente como fuente de contexto de la nueva raíz. Es una herramienta de precisión que usan por dentro utilidades como los portales o el mapeo de listas, para que un árbol renderizado en un punto vea los proveedores de otro. Cuidado con la confusión habitual: ese segundo argumento no hace que la raíz se disponga con el owner que le pasas —la disposición sigue siendo tuya o del onCleanup que coses—; solo redirige de quién hereda el contexto. Reparentar contexto y heredar muerte son, otra vez, dos cosas distintas.
La simetría es total y merece verse de frente. Un createEffect con su onCleanup describe una correspondencia entre estado y mundo: mientras esto valga aquello, sostén este recurso. El cuerpo del efecto establece la correspondencia; el onCleanup la deshace. dispose no es más que ese deshacer aplicado a un árbol entero de una vez: recorre todas las correspondencias vivas bajo la raíz y las rompe en el orden correcto, hijos antes que padres, para que ninguna se apoye en algo que ya no existe. Por eso disponer es montar leído al revés, y por eso el mismo mecanismo —el árbol de owners con sus listas de limpieza— sirve tanto para el desmontaje de un componente como para el cierre de una raíz que abriste en un test. Cuando en un componente no dispones nada, no es que la disposición no ocurra: es que el runtime firmó el dispose por ti al montar y lo ejecutó por ti al desmontar. Aprender a llamarlo a mano no añade una mecánica nueva; te devuelve la autoría de una que siempre estuvo ahí. Y con esa autoría llega la responsabilidad que define a un programador de Solid maduro: cada raíz que abres es una deuda de disposición, y saldarla —a mano en un test, o cosiéndola a un owner con onCleanup— es lo que separa una aplicación que respira de una que se hincha hasta ahogarse.
- Arranca un
setIntervaldentro de un efecto en una raíz manual, con suonCleanup; dispón y confirma en consola que los ticks paran. - Escribe un test con
createRootque cree un memo, verifique dos valores tras cambiar una señal y disponga al final; comprueba que otro test no ve rastro del anterior. - Llama a
disposedos veces seguidas y verifica que la segunda no lanza ni ejecuta limpiezas de nuevo. - Monta una raíz dentro de un componente y átala con
onCleanup(dispose); desmonta el componente y comprueba que la raíz muere con él. - Diseña un efecto one-shot que se disponga a sí mismo la primera vez que una señal alcanza un umbral; razona por qué conviene disponer al final de la corrida.