wandres.dev
OWNERSHIP Y CLEANUP · onCleanup, getOwner

getOwner y runWithOwner

El owner solo está activo durante la ejecución síncrona; tras un await se pierde. getOwner captura el dueño en el momento correcto y runWithOwner vuelve a entrar en su contexto para que efectos, limpiezas y contexto se enganchen donde deben.

⏱ 14 min

El árbol de ownership tiene un talón de Aquiles: solo existe durante la ejecución síncrona. En el instante en que tu código cede el control —un await, un then, un setTimeout, el callback de un evento diferido—, la variable del owner actual vuelve a estar vacía, y todo lo reactivo que crees a partir de ahí nace huérfano. Solid resuelve esto con dos primitivas quirúrgicas: getOwner, que fotografía el dueño en el momento correcto, y runWithOwner, que te devuelve a su contexto cuando lo necesites. Son la herramienta imprescindible para que lo asíncrono no rompa la disposición automática.

🎯 Al terminar esta lección sabrás
  • Entender por qué el owner actual solo vive durante la ejecución síncrona.
  • Usar getOwner para capturar el dueño antes de un salto asíncrono.
  • Usar runWithOwner para re-entrar en ese contexto y enganchar efectos y limpiezas.
  • Reconocer los escenarios donde hace falta: promesas, temporizadores y callbacks externos.

El owner vive solo en lo síncrono

Solid fija la variable del owner actual justo antes de correr el cuerpo de una computación y la restaura al terminar. Ese “terminar” es el final de la ejecución síncrona. Un await parte la función en dos: lo que hay antes corre con el owner puesto; lo que hay después corre más tarde, en otro turno del bucle de eventos, cuando el owner ya volvió a ser null.

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

function Grafica(props) {
  cargar(props.id).then((serie) => {
    // AQUI el owner ya es null: estamos en otro turno del event loop
    createEffect(() => dibujar(serie, tema())); // HUERFANO: no se dispone
    onCleanup(() => destruir());                 // NO se agenda: sin owner
  });

  return <canvas />;
}

El efecto creado dentro del then no pertenece al owner de Grafica: cuando el componente se desmonte, nadie lo dispondrá. Seguirá vivo, reaccionando a tema, dibujando sobre un canvas que ya no existe. Es una fuga silenciosa, y el onCleanup de al lado ni siquiera llega a agendarse.

getOwner: fotografiar el dueño

getOwner() devuelve el owner activo en el instante en que lo llamas —o null si no hay ninguno—. La clave es cuándo lo llamas: tienes que hacerlo mientras el owner sigue puesto, es decir, de forma síncrona en el cuerpo del componente o del efecto, antes del salto asíncrono. Guardas esa referencia en una constante y la conservas para más tarde.

import { getOwner } from "solid-js";

function Grafica(props) {
  const owner = getOwner();      // capturado AHORA, con el owner aun activo

  cargar(props.id).then((serie) => {
    // owner sigue apuntando al dueno de Grafica, aunque ya no este activo
  });

  return <canvas />;
}

getOwner no cambia nada del sistema: solo lee. Su valor es una referencia al nodo del árbol que veíamos en la primera lección, con sus hijos, sus limpiezas y su contexto. Tenerla en la mano es tener la llave para volver.

runWithOwner: re-entrar en el contexto

runWithOwner(owner, fn) ejecuta fn como si el owner activo fuese el que le pasas. Durante esa ejecución, cualquier createEffect, createMemo u onCleanup se engancha a ese owner; el contexto de createContext vuelve a estar disponible y los límites de error del subárbol se restauran. Devuelve lo que devuelva fn. Combinada con getOwner, cierra el agujero asíncrono:

import { getOwner, runWithOwner, createEffect, onCleanup } from "solid-js";

function Grafica(props) {
  const owner = getOwner();

  cargar(props.id).then((serie) => {
    runWithOwner(owner, () => {
      createEffect(() => dibujar(serie, tema())); // ahora es hijo de Grafica
      onCleanup(() => destruir());                 // ahora SI se agenda
    });
  });

  return <canvas />;
}

Ahora el efecto y su limpieza pertenecen al owner de Grafica: cuando el componente se desmonte, Solid dispondrá el subárbol y destruir correrá. La disposición automática vuelve a funcionar pese al await de por medio.

flowchart LR
S[cuerpo sincrono owner activo] --> G[getOwner captura el dueno]
S --> A[await rompe el contexto]
A --> N[owner actual es null]
N --> R[runWithOwner reentra]
G -.pasa la referencia.-> R
R --> E[efecto y cleanup enganchados al dueno]
style S fill:#89b4fa,color:#11111b
style N fill:#f38ba8,color:#11111b
style R fill:#a6e3a1,color:#11111b
style E fill:#a6e3a1,color:#11111b

El patrón no se limita a las promesas. Sirve siempre que quieras crear reactividad desde un callback que Solid no controla: el registro de eventos de una librería de terceros, un setTimeout, el resolvedor de un evento personalizado. Capturas el owner arriba y envuelves en runWithOwner el trozo que crea efectos o limpiezas.

Un caso real: puentear un emisor externo

Imagina una librería de sincronización que te avisa por un callback cuando llegan datos, y quieres crear reactividad nueva cada vez que eso ocurre. Ese callback lo invoca la librería, fuera de todo owner; sin re-entrar, cualquier efecto que crees dentro será huérfano.

import { getOwner, runWithOwner, createSignal, createEffect } from "solid-js";

function Sincronizado(props) {
  const owner = getOwner();
  const cliente = conectar(props.sala);

  cliente.alRecibir((datos) => {
    runWithOwner(owner, () => {
      const [estado, setEstado] = createSignal(datos);
      createEffect(() => pintar(estado()));   // hijo de Sincronizado
    });
  });

  onCleanup(() => cliente.desconectar());       // sincrono: no necesita re-entrar
  return <section />;
}

Fíjate en el reparto de responsabilidades: la desconexión del cliente se registra de forma síncrona en el cuerpo —tiene owner, no necesita nada especial—, mientras que lo que nace dentro del callback asíncrono se envuelve en runWithOwner. Cada pieza se engancha al mismo owner de Sincronizado, y todo se dispone junto al desmontar.

Cuando el salto puede tardar, protege la re-entrada con una condición de vida, para no crear efectos en un ámbito ya difunto:

import { getOwner, runWithOwner, onCleanup } from "solid-js";

const owner = getOwner();
let vivo = true;
onCleanup(() => (vivo = false));   // se apaga al desmontar

tarea().then((r) => {
  if (!vivo) return;               // no re-entres en un owner muerto
  runWithOwner(owner, () => aplicar(r));
});
💡
Un owner dispuesto sigue siendo re-entrable, pero está muerto

runWithOwner no comprueba si el owner ya fue dispuesto: ejecutará fn de todas formas. Si el componente se desmontó durante el await, estarás creando efectos dentro de un ámbito difunto, que no volverá a disponerse y puede volver a fugar. Cuando el salto asíncrono pueda ser largo, guarda un flag de “vivo” cerrado por un onCleanup síncrono, o comprueba una condición de montaje antes de re-entrar. La regla sana: re-entra solo si sigues necesitando el resultado.

ℹ️
createResource ya hace esto por ti

Buena parte del trabajo asíncrono en Solid no necesita runWithOwner a mano, porque createResource, Suspense y las utilidades de datos ya capturan y restauran el owner internamente. getOwner y runWithOwner son las herramientas de bajo nivel para cuando sales de esos caminos: integraciones con librerías imperativas, APIs del navegador basadas en callbacks, o flujos asíncronos hechos a mano.

El contexto reactivo no es ambiental, es capturable

La lección profunda de este par de primitivas es que en Solid “estar dentro de un componente” no es una propiedad ambiental que flota en el aire mientras el componente existe, sino un valor concreto y capturable: el owner. En React, hooks como useEffect funcionan por su posición en el árbol de llamadas del render, y por eso no puedes invocarlos tras un await —no hay forma de recuperar la posición—. Solid materializa ese contexto en un objeto que puedes guardar en una variable, pasar por parámetro y volver a activar cuando te convenga. Eso convierte lo asíncrono, que en otros modelos es una zona prohibida para la creación de estado, en territorio perfectamente habitable: el dueño no se pierde porque el tiempo pase, solo deja de estar activo, y reactivarlo es un runWithOwner. Esta cosificación del ámbito —volver dato lo que en otros frameworks es magia posicional— es la misma idea que hace que un signal sea una función que puedes pasar, o que un efecto sea un nodo del árbol que puedes inspeccionar. Cuando entiendes que el owner es un valor de primera clase, dejas de temer el await y empiezas a tratar la disposición como algo que tú controlas, no que se te escapa.

⚔️ Recupera el dueño tras el salto
  1. Reproduce la fuga: crea un efecto dentro de un then sin runWithOwner y confirma con el aviso de consola que nace huérfano.
  2. Captura el owner con getOwner antes de la promesa y envuelve la creación del efecto en runWithOwner; verifica que ahora se dispone al desmontar.
  3. Haz lo mismo desde un setTimeout en lugar de una promesa y comprueba que el mecanismo es idéntico.
  4. Añade una guarda que evite re-entrar si el componente ya se desmontó, usando un flag cerrado en un onCleanup síncrono.
  5. Explica en una frase por qué getOwner debe llamarse antes del await y nunca después.