Estado global disposable con getOwner
Un signal de módulo vive para siempre; a veces quieres un singleton reactivo que puedas destruir y recrear —al cerrar sesión, en HMR, entre tests—. createRoot le da una raíz disponible, getOwner captura esa raíz y runWithOwner permite colgar de ella nuevas computaciones que morirán con el singleton.
Un singleton dentro de una raíz
El estado global disponible es un módulo que crea su store dentro de createRoot, expone getters y acciones, y guarda además el dispose de la raíz para poder tirar todo abajo. Inicializarlo de forma perezosa evita construirlo antes de que alguien lo use —y, en SSR, antes de que exista una petición.
// store/sesion.tsimport { createRoot, createSignal, getOwner, type Owner } from "solid-js";type Sesion = { usuario: () => string | null; entrar: (nombre: string) => void; salir: () => void; owner: Owner | null; dispose: () => void;};let instancia: Sesion | null = null;export function usarSesion(): Sesion { if (instancia) return instancia; instancia = createRoot((dispose) => { const [usuario, setUsuario] = createSignal<string | null>(null); return { usuario, entrar: (nombre) => setUsuario(nombre), salir: () => setUsuario(null), owner: getOwner(), // capturamos la raiz para colgarle efectos luego dispose, }; }); return instancia;}Cualquier componente que llame a usarSesion() recibe el mismo objeto: un singleton reactivo. La diferencia con un signal de módulo pelado es que este trae un dispose en la mano y una owner capturada.
La inicialización perezosa no es un adorno. Construir el store dentro de usarSesion, y solo la primera vez, significa que ningún efecto arranca hasta que alguien de verdad lo pide: no pagas por lo que no usas. Y, sobre todo, es la cortesía mínima frente a SSR: un módulo que crea su reactividad al importarse la crearía una vez por proceso, compartida entre peticiones; uno que la crea perezosamente te da al menos el punto de control donde decidir si esa instancia debe ser por-proceso o por-petición.
getOwner y runWithOwner
getOwner devuelve el owner activo en el punto donde lo llamas; dentro del callback de createRoot, eso es la raíz del singleton. Guardarlo te da un asa a un ámbito que, de otro modo, quedaría cerrado tras salir del callback. runWithOwner(owner, fn) es la operación inversa: reinstala ese owner, ejecuta fn bajo él y restaura el anterior. Las computaciones creadas dentro de fn pertenecen a la raíz capturada —y, por tanto, mueren cuando el singleton se dispone—, aunque las hayas escrito lejos, en un efecto de arranque o en un módulo distinto.
import { runWithOwner, createEffect } from "solid-js";// En cualquier parte, mas tarde: registrar un efecto global// que persista datos, pero atado a la vida del singleton.const sesion = usarSesion();runWithOwner(sesion.owner, () => { createEffect(() => { const u = sesion.usuario(); localStorage.setItem("usuario", u ?? ""); });});Sin runWithOwner, ese createEffect no tendría owner —o pertenecería al componente que lo escribió, muriendo antes de tiempo—. Con él, el efecto cuelga de la raíz del singleton: vive tanto como el estado global y desaparece con él en un solo dispose.
Hay una simetría limpia entre las dos primitivas. getOwner lee el owner activo; runWithOwner lo instala temporalmente para ejecutar algo bajo él. Juntas convierten el owner en un valor de primera clase —guardable, pasable, reutilizable— y rompen la atadura entre dónde escribes una computación y a qué vida pertenece. Es el mismo desacople que createRoot hizo entre poseer y rastrear, ahora aplicado a la dimensión temporal: la posición sintáctica del código deja de fijar su ciclo de vida. Ese desacople es lo que permite un store global cuya raíz vive en un módulo pero que acepta efectos aportados desde componentes, plugins o cualquier rincón, todos condenados a morir juntos cuando llegue el dispose.
Un ejemplo concreto: un sistema de plugins donde cada plugin, al instalarse, aporta efectos de sincronización al store de sesión sin conocer su raíz. El núcleo expone runWithOwner(sesion.owner, fn) como punto de extensión; cada plugin entrega su fn, sus efectos cuelgan de la sesión, y al cerrarla todos se van juntos sin que ningún plugin haya tenido que registrar su propia limpieza. La disposición centralizada deja de ser una convención frágil y pasa a ser una propiedad de la arquitectura.
Una advertencia de higiene, eso sí: repartir el owner crudo en la API pública invita a que cualquiera cuelgue lo que quiera del singleton, y con ello a acoplamientos difíciles de rastrear. En bases de código grandes conviene ofrecer una función acotada —un registrarEfectoDeSesion(fn) que encapsule el runWithOwner— y documentar qué se permite adjuntar, en lugar de exponer el owner desnudo. El owner es un asa poderosa; precisamente por eso merece un mango con forma, no un filo suelto.
Destruir y recrear limpio
La ganancia se cobra en el ciclo de vida. Cerrar sesión no es solo poner el usuario a null: es tirar abajo toda la reactividad de la sesión —efectos de persistencia, sincronizaciones, sondeos— para que nada del usuario anterior sobreviva al siguiente. Con el singleton disponible, es una línea.
export function cerrarSesion() { instancia?.dispose(); // mata la raiz entera y todo lo colgado de ella instancia = null; // el proximo usarSesion() construye una raiz nueva}Tras dispose, el siguiente usarSesion() encuentra instancia en null y reconstruye un ámbito virgen. Ningún efecto zombi del ciclo anterior queda suscrito. El mismo patrón resuelve el hot module replacement: al recargar el módulo en caliente, dispón la instancia vieja antes de que la nueva la reemplace, y evitarás la acumulación de estados fantasma que ensucia el desarrollo con recargas.
// Cierra la raiz previa cuando el bundler recarga este modulo en caliente.if (import.meta.hot) { import.meta.hot.dispose(() => { instancia?.dispose(); instancia = null; });}Sin esta guarda, cada recarga en caliente dejaría atrás una raíz completa —con sus efectos y suscripciones— viva y suscrita; tras media hora programando tendrías decenas de sesiones fantasma reaccionando a la vez. La disponibilidad no solo sirve para el logout del usuario final: sirve, a diario, para que tu propia sesión de desarrollo no se degrade recarga a recarga.
En los tres casos —cierre de sesión, recarga en caliente, aislamiento entre tests— el gesto es idéntico: dispose y vuelta a null. Que el mismo par de líneas resuelva situaciones tan distintas no es casualidad, sino la señal de que has dado con la primitiva correcta en lugar de con un parche para cada una. Un buen diseño se reconoce por eso: el mismo mecanismo cubre casos que parecían pedir soluciones separadas.