createRoot: fabricar un scope reactivo
createRoot abre a mano un owner reactivo fuera de todo componente: recibe un callback con dispose, devuelve lo que ese callback retorne, posee pero no rastrea y no se auto-dispone. Es la primitiva que silencia el aviso de computaciones huérfanas y la raíz sobre la que se construye toda reactividad de larga vida.
La firma: un callback que recibe dispose
createRoot toma una función y la ejecuta de inmediato, pasándole un único argumento: dispose, la función que destruye todo lo creado dentro del ámbito. Lo que tu callback devuelva es lo que createRoot devuelve, así que la usas como un embudo para sacar del ámbito exactamente lo que quieras conservar.
import { createRoot, createSignal, createEffect } from "solid-js";const api = createRoot((dispose) => { const [count, setCount] = createSignal(0); createEffect(() => console.log("count:", count())); // El valor de retorno es lo que createRoot entrega hacia fuera. return { count, setCount, dispose };});api.setCount(1); // el efecto sigue vivo y reacciona: "count: 1"api.dispose(); // se cierra el ambito: el efecto muereapi.setCount(2); // silencio; el signal existe, pero ya nadie lo observaEl efecto vive dentro de la raíz. Mientras no la dispongas, reacciona a count con total normalidad, igual que si estuviera en un componente. En cuanto invocas dispose, Solid destruye ese efecto —y cualquier memo, cualquier onCleanup y cualquier raíz hija que hubieras anidado— y el signal queda inerte: persiste como valor en memoria, pero sin observadores vivos su cambio no propaga a nadie.
createRoot ejecuta el callback de forma ávida: no aplaza nada, no espera a un montaje. En el momento en que la llamada retorna, el efecto interno ya corrió su primera vez y el objeto que devolviste está listo para usarse. Por eso cristalizan dos convenciones de retorno que cubren casi todo: devolver una API —getters y acciones, con el dispose incluido para cerrar cuando toque— o devolver solo el dispose cuando el ámbito trabaja por sus efectos secundarios y no expone ninguna lectura. La elección la dicta una pregunta: ¿alguien de fuera necesita leer este estado, o solo necesita poder apagarlo?
Poseer no es rastrear
Una raíz es un owner que posee computaciones pero no rastrea señales, y que no se dispone solo. Conviene separar con bisturí dos papeles que la palabra «reactivo» amontona.
- Poseer es acumular hijos —efectos, memos, limpiezas, subraíces— en una lista para destruirlos en bloque. Eso sí lo hace la raíz: es un nodo del árbol de ownership.
- Rastrear es suscribirse a las señales que se leen. Eso no lo hace la raíz: el cuerpo del callback corre sin listener activo, de modo que leer
count()en el nivel superior de la raíz no crea ninguna dependencia.
createRoot((dispose) => { const [n] = createSignal(0); console.log(n()); // lectura NO rastreada: no suscribe a nada createEffect(() => n()); // aqui SI: el efecto es quien rastrea dispose();});Por eso el nombre root: es la cima de un subárbol, un contenedor de vidas, no un reactor. Rastrear es tarea de las hojas que cuelgan de ella —los efectos y memos—, no del tronco.
Si abrieras la caja, un owner resulta ser una estructura modesta: una lista de las computaciones que posee, una lista de limpiezas pendientes, un puntero a su owner padre y el contexto que hereda. Ni una señal, ni una suscripción; esas viven en el grafo de dependencias, que es la otra estructura. Una raíz es, sencillamente, el nodo de esa jerarquía de posesión al que nadie apunta como padre disponible.
// Forma aproximada de un Owner en Solid, simplificada.type Owner = { owned: Computation[] | null; // hijos que moriran con el cleanups: (() => void)[] | null; // los onCleanup registrados owner: Owner | null; // padre en el arbol de ownership context: Record<symbol, unknown> | null; // contexto heredado};Ver esa forma desmitifica todo lo demás: dispose recorre owned, ejecuta cleanups, y desengancha las computaciones del grafo. No hay más magia que una lista y un recorrido.
El aviso que silencia
Si creas un createEffect o un createMemo sin ningún owner activo —en el nivel de un módulo, dentro de un setTimeout, en una callback suelta— Solid te avisa por consola en desarrollo: computations created outside a createRoot or render will never be disposed. No es una advertencia decorativa: sin owner, esa computación no tiene quién ejecute su limpieza ni quién la destruya, así que vivirá y seguirá suscrita hasta que el programa termine. Es una fuga por construcción.
createRoot es la respuesta canónica: instala un owner, la computación cae dentro de él, el aviso desaparece y —lo esencial— ahora existe un dispose que puede matarla cuando toque.
Merece la pena entender por qué el aviso solo aparece en desarrollo y no en producción. No es un error recuperable ni una condición que Solid pueda arreglar por ti: es un recordatorio dirigido al programador de que ha creado vida reactiva sin firmar su muerte. En el build de producción el mensaje se elimina para no pesar, pero la fuga que denunciaba seguiría ahí, silenciosa. Tratar ese aviso como ruido y acallarlo es, muy literalmente, apagar el detector de humos porque suena.
Qué hereda y qué corta
La raíz no queda del todo aislada: hereda el contexto del owner en cuyo seno la creaste, de modo que useContext dentro de ella sigue viendo los mismos proveedores. Lo que corta es la disposición automática: aunque exista ese vínculo de contexto con el owner de arriba, la raíz no se apunta en la lista de hijos de nadie, así que destruir al padre no la destruye a ella. Ese es justo el sentido de «raíz»: una vida que decides tú, no el árbol que la rodea.
En la práctica, esa herencia de contexto es lo que permite fabricar una raíz dentro de un componente y seguir consumiendo sus proveedores sin más ceremonia:
import { createRoot, useContext, onCleanup } from "solid-js";function dentroDeUnComponente() { // La raiz hereda el contexto del componente que la abre... const dispose = createRoot((d) => { const tema = useContext(TemaContext); // ...por eso esto resuelve bien createEffect(() => document.body.dataset.tema = tema()); return d; }); onCleanup(dispose); // pero su muerte hay que atarla a mano}Contexto sí, muerte no: esa asimetría exacta es la que hace de createRoot la herramienta correcta para ámbitos de larga vida que aun así deben respetar los proveedores vigentes, y la que te obliga, a cambio, a coser su disposición donde corresponda.
La misma lógica se aplica hacia dentro cuando anidas raíces. Nada impide abrir una raíz dentro de otra; es más, cada componente de tu aplicación ya es una raíz anidada bajo la de render, aunque el runtime la gestione por ti. Cuando anidas a mano, recuerda la asimetría del apartado anterior aplicada hacia dentro: una raíz interna no se dispone cuando se dispone la externa, porque tampoco figura en su lista de hijos. Si quieres que la interna muera con la externa, cóselas con onCleanup.
createRoot((disposeExterno) => { // Esta raiz interna sobreviviria a disposeExterno si no la atamos: const disposeInterno = createRoot((d) => { createEffect(() => {/* ... */}); return d; }); onCleanup(disposeInterno); // ahora si: muere junto con la externa return disposeExterno;});Esta es la excepción que confirma la regla, y la razón de que un componente hijo sí muera con su padre: el runtime, al montar, teje esa costura por ti en cada nivel del árbol. Fabricar raíces a mano es, sencillamente, aceptar tejerla tú donde haga falta.