wandres.dev
OWNERSHIP Y CLEANUP · onCleanup, getOwner

El árbol de ownership

Cada computación en Solid nace con un dueño: el owner en cuyo contexto se ejecutó. Los owners forman un árbol paralelo al del DOM que gobierna la disposición jerárquica, la limpieza en cascada y el alcance del contexto y los errores.

⏱ 13 min

Detrás de la reactividad de grano fino late una segunda estructura, menos visible pero igual de decisiva: el árbol de ownership. Cada vez que Solid ejecuta una computación —un efecto, un memo, la raíz de un render— abre un ámbito que recuerda qué nació dentro de él y qué habrá que liberar cuando muera. Ese ámbito es el owner. No se importa, no se instancia a mano, no aparece en tu código; y sin embargo es la columna vertebral que hace que la memoria y la limpieza se gestionen solas. Comprenderlo es pasar de usar onCleanup por receta a saber con exactitud por qué y cuándo se dispara.

🎯 Al terminar esta lección sabrás
  • Entender qué es un owner y qué guarda: hijos, limpiezas, contexto y su propio padre.
  • Ver cómo cada computación registra su owner y teje un árbol paralelo al del DOM.
  • Comprender la disposición jerárquica: destruir un owner limpia su subárbol de abajo arriba.
  • Situar createRoot como la raíz explícita que abre una frontera de disposición.

Qué es un owner

Un owner es un nodo con cuatro piezas y nada más. Guarda una lista de computaciones hijas —lo que se creó dentro de él—, una lista de funciones de limpieza —las que registró onCleanup—, un puntero a su owner padre, y el contexto, los valores que createContext propaga hacia abajo. Esa parquedad es justo lo que permite que la disposición sea determinista y barata.

// forma conceptual del nodo owner en el runtime de Solid
interface Owner {
  owned: Computation[] | null;              // computaciones creadas aqui dentro
  cleanups: (() => void)[] | null;          // limpiezas registradas con onCleanup
  owner: Owner | null;                      // el owner padre
  context: Record<symbol, unknown> | null;  // valores de contexto heredados
}

Toda computación —createEffect, createMemo, createRenderEffect, createComputed— es a la vez observadora del grafo reactivo y owner: puede tener hijos y limpiezas propias. Un signal, en cambio, no es un owner: es solo un dato observable, una fuente sin descendencia ni ciclo de vida. Por eso hablamos de “el dueño de un efecto”, nunca de “el dueño de un signal”.

Un matiz que conviene fijar pronto: un memo es un híbrido. Como owner posee hijos y limpiezas; como fuente otros pueden leerlo y suscribirse a él. Un createMemo, por tanto, contiene efectos anidados que se disponen con él y a la vez notifica a quienes lo consumen cuando su valor cambia. Un efecto, en cambio, es una hoja del grafo de dependencias —nadie lo lee— aunque sea un nodo interno del árbol de owners con sus propios descendientes.

Cómo se teje el árbol

Solid mantiene una variable interna con el owner actual. Antes de ejecutar el cuerpo de una computación, apunta esa variable a su nodo; cualquier computación creada durante esa ejecución lee la variable y se registra como hija. Al terminar, restaura el owner anterior. Es el mismo mecanismo de pila que gobierna el ámbito léxico de un lenguaje, pero aplicado a la vida de los recursos reactivos.

import { createEffect, createMemo } from "solid-js";

function Panel() {
  // owner activo: el del componente Panel
  const doble = createMemo(() => base() * 2); // hijo del owner de Panel

  createEffect(() => {          // hijo del owner de Panel
    createEffect(() => {        // NIETO: hijo del efecto exterior
      registrar(doble());
    });
  });

  return <div>{doble()}</div>;
}

El efecto interior no es hermano del exterior: es su hijo. Cada nivel de anidamiento del código se corresponde con un nivel del árbol de owners. El render del componente, a su vez, cuelga del owner que lo montó, y así sucesivamente hasta la raíz que abrió render o createRoot.

flowchart TD
Root[createRoot raiz de la app] --> App[owner de App]
App --> Panel[owner de Panel]
Panel --> Memo[memo doble]
Panel --> E1[efecto exterior]
E1 --> E2[efecto interior nieto]
E1 --> Cl[cleanup del efecto exterior]
style Root fill:#f9e2af,color:#11111b
style Panel fill:#89b4fa,color:#11111b
style E1 fill:#cba6f7,color:#11111b
style E2 fill:#cba6f7,color:#11111b
style Cl fill:#f38ba8,color:#11111b

Este árbol corre en paralelo al del DOM, pero no es idéntico: un componente puede abrir varios owners anidados —uno por cada control de flujo—, y una computación sin salida visual, como un efecto de sincronización, es un nodo del árbol de owners aunque no le corresponda ningún nodo del DOM.

La disposición jerárquica

Cuando un owner se destruye, Solid recorre su subárbol y lo desmonta en profundidad: primero dispone recursivamente a cada hijo y solo después ejecuta sus propias limpiezas. El recorrido va en orden inverso al de creación —LIFO—, de modo que lo último que se montó es lo primero en desmontarse, como al deshacer una pila. Así, un recurso que dependía de otro creado antes se libera primero.

La disposición es síncrona y en profundidad: cuando se dispara, Solid no la difiere ni la reparte en el tiempo. Recorre el subárbol completo de una vez, desconecta cada computación de las fuentes que observaba —para que deje de recibir notificaciones— y ejecuta sus limpiezas. Al volver de ese recorrido, el subárbol entero ha dejado de existir para el sistema reactivo: ni reacciona, ni retiene, ni notifica.

ℹ️
Por qué importa el orden

Que la limpieza sea LIFO no es un capricho: respeta las dependencias de construcción. Si en un efecto abres una conexión y luego registras un observador sobre ella, quieres retirar el observador antes de cerrar la conexión. Al invertir el orden de las limpiezas, Solid deshace la pila exactamente en el sentido correcto sin que tengas que pensarlo.

Este barrido es lo que vuelve fiable a onCleanup: no orquestas el desmontaje pieza por pieza. Si un componente sale del DOM —porque un Show cambió de rama o un For perdió un elemento—, Solid dispone el owner de ese subárbol y todas las limpiezas de sus descendientes se ejecutan en cascada, sin intervención tuya.

createRoot es la única forma de abrir una raíz a mano: crea un owner sin padre —una frontera de disposición— y te entrega una función dispose para cerrarlo cuando quieras. Todo lo demás cuelga, directa o indirectamente, de una raíz creada por render o por createRoot.

import { createRoot, createEffect } from "solid-js";

const dispose = createRoot((dispose) => {
  createEffect(() => sincronizar(estado()));
  return dispose;               // controlas tu la destruccion del ambito
});

// mas tarde, cuando ya no lo necesites:
dispose();                      // corre todas las limpiezas del subarbol

Contexto y errores suben por el árbol

El árbol de owners no solo gobierna la disposición: es también el canal por el que viajan el contexto y los errores. Cada owner guarda su campo context, y cuando un componente llama a useContext, Solid no consulta el árbol del DOM, sino que trepa de padre en padre por el árbol de owners hasta encontrar el proveedor más cercano que fijó ese valor. La herencia de contexto es la estructura de pertenencia recorrida hacia arriba.

import { createContext, useContext } from "solid-js";

const Tema = createContext(() => "claro");

function Boton() {
  const tema = useContext(Tema); // sube por owners hasta el proveedor mas cercano
  return <button class={tema()}>ok</button>;
}

Los límites de error operan sobre el mismo árbol en sentido inverso: instalan un manejador en su owner, y cuando una computación descendiente lanza una excepción, el error asciende de hijo a padre hasta el primer manejador que lo capture. Disposición, contexto y errores comparten una sola columna vertebral, y esa es la razón de que capturar el owner —lo que harás con getOwner en la próxima lección— te dé acceso a las tres cosas de golpe: no capturas “un ámbito de limpieza”, capturas el nodo entero con su posición en el árbol.

📝
Un árbol, tres responsabilidades

Cuando pienses en el owner, no lo reduzcas a “donde se guardan los onCleanup”. El mismo nodo resuelve tres preguntas distintas: qué se destruye cuando esto muere (disposición), qué valores de contexto ve este código (herencia), y quién captura sus excepciones (errores). Tres sistemas que otros frameworks implementan por separado son, en Solid, tres lecturas de un único árbol.

🌳

Owner

Ámbito con hijos, limpiezas, contexto y padre. Toda computación es uno; un signal no.

🧬

Computación

Efecto o memo: observa el grafo y además posee hijos. Vive colgada de su owner.

🪝

onCleanup

Empuja una función a la lista de limpiezas del owner actual. Corre al re-ejecutar o al disponer.

✂️

createRoot

Abre una raíz sin padre y devuelve dispose. La única frontera de disposición manual.

El ownership es la mitad invisible de la reactividad

Solemos contar Solid como “un grafo de signals que propaga cambios”, y es verdad, pero es solo la mitad de la historia. Junto al grafo de dependencias —quién lee a quién— existe un árbol de pertenencia —quién vive dentro de quién—, y son estructuras distintas con propósitos distintos. El grafo de dependencias decide qué se recalcula cuando algo cambia; el árbol de owners decide qué se destruye cuando algo muere. React no tiene un equivalente real de esto: su desmontaje se apoya en la reconciliación del virtual DOM y en el array de dependencias de cada useEffect, que tú debes mantener a mano. Solid lo saca del componente y lo convierte en una propiedad estructural del sistema: cada recurso pertenece a un ámbito, cada ámbito conoce a sus hijos y a sus limpiezas, y la destrucción es un recorrido determinista de ese árbol. Por eso en Solid casi nunca escribes lógica de desmontaje explícita: la escribiste ya, sin saberlo, en el mismo lugar donde creaste el recurso. Interiorizar que existen dos estructuras —el grafo que reacciona y el árbol que limpia— es lo que separa a quien memoriza onCleanup de quien entiende por qué su intervalo se detiene solo.

⚔️ Recorre el árbol con tus manos
  1. Escribe un componente con un memo y dos efectos anidados; pon un console.log distinto en cada uno y dibuja en papel el árbol de owners resultante.
  2. Registra un onCleanup en el efecto interior y otro en el exterior; desmonta el componente y anota el orden en que se imprimen. Confirma que es LIFO.
  3. Crea un ámbito con createRoot, mete dentro un efecto con su limpieza y llama a dispose. Verifica que la limpieza corre al disponer, no antes.
  4. Explica en una sola frase por qué un signal no aparece nunca como nodo del árbol de owners.