Signals fuera de componentes
Definir signals a nivel de módulo para compartir estado global sin librerías: por qué funciona (los signals no necesitan owner, solo las computaciones lo necesitan), el patrón de módulo con acciones, su ciclo de vida sin disposición y el peligro del estado compartido entre peticiones en SSR.
Nada obliga a que un signal viva dentro de un componente. Puedes crearlo en la raíz de un módulo, exportarlo y compartirlo entre toda la aplicación: así de simple es el “estado global” en Solid, sin reducers, sin providers, sin librerías. Pero esa libertad descansa sobre una asimetría precisa del sistema —qué necesita un dueño y qué no— y esconde una trampa seria en cuanto tu código corre en un servidor.
- Crear estado compartido definiendo signals a nivel de módulo.
- Encapsularlo con el patrón de módulo: getter de solo lectura más acciones.
- Entender por qué los signals no necesitan un owner y las computaciones sí.
- Conocer su ciclo de vida y el peligro del estado global en SSR.
Estado compartido en un módulo
Un signal creado en la cima de un módulo existe una sola vez para todo el programa. Cualquier componente que importe su getter y lo lea en su JSX quedará suscrito; cualquiera que llame al setter actualizará a todos a la vez. Es un bus de estado global de una sencillez casi ofensiva:
// store/contador.ts
import { createSignal } from "solid-js";
const [count, setCount] = createSignal(0);
export { count }; // lectura publica
export const incrementar = () => setCount(c => c + 1);
export const reiniciar = () => setCount(0);
// cualquier componente, en cualquier parte del arbol
import { count, incrementar } from "../store/contador";
function Boton() {
return <button onClick={incrementar}>Total: {count()}</button>;
}
Fíjate en el patrón: exportas el getter para leer y unas acciones con nombre para escribir, pero te guardas el setter dentro del módulo. Nadie de fuera puede mutar el estado de formas no previstas; toda escritura pasa por una función con intención explícita. Es encapsulación clásica, lograda sin clases ni ceremonia.
Por qué funciona: quién necesita un owner
Aquí conviene una distinción fina que casi nadie explica. En Solid, las computaciones —efectos y memos— viven dentro de un árbol de ownership: alguien es su dueño y se encargará de limpiarlas con onCleanup cuando ese dueño se destruya. Los signals, en cambio, no necesitan dueño: son solo un valor con una lista de observadores. Por eso puedes crear un signal en cualquier parte, incluido el nivel de módulo, sin envolverlo en nada.
flowchart TD A[createSignal a nivel de modulo] --> B[Valido: un signal no necesita owner] C[createEffect a nivel de modulo] --> D[Aviso: computacion sin root, nunca se limpia] D --> E[Envuelvelo en createRoot para darle un dueno]
La contrapartida aparece si intentas crear una computación a nivel de módulo. Un createEffect o un createMemo fuera de todo componente no tiene dueño que lo limpie, y Solid te avisará por consola de que una computación creada fuera de una raíz nunca se dispondrá. La solución es envolver ese estado reactivo global en createRoot, que crea un dueño de larga vida a propósito:
import { createRoot, createMemo } from "solid-js";
// Un memo global necesita una raiz que lo posea:
const dobleGlobal = createRoot(() => createMemo(() => count() * 2));
Ciclo de vida: viven mientras viva la app
Un signal de módulo no se crea ni se destruye con los componentes: nace la primera vez que se importa el módulo y vive hasta que el programa termina. No tiene onCleanup, no participa en ningún ciclo de montaje. Esto es exactamente lo que quieres para estado verdaderamente global —el usuario autenticado, el tema, un carrito— y exactamente lo que no quieres para estado efímero de una vista.
Este es el peligro que arruina despliegues. En el navegador, un módulo vive por usuario, porque cada pestaña es un programa aparte. En un servidor SSR —SolidStart— el mismo proceso atiende a muchos usuarios, y un signal de módulo es uno solo compartido por todas las peticiones. Si guardas ahí el usuario logueado, los datos de Ada pueden filtrarse en la respuesta de Alan. Regla de oro: el estado global de módulo es seguro para constantes y configuración, pero el estado por-usuario debe vivir en el contexto de la petición, casi siempre vía createContext o los mecanismos de SolidStart, nunca en una variable de módulo mutable.
Venir de otros frameworks deja una creencia pegajosa: que el estado “pertenece” a un componente y que sacarlo de ahí requiere maquinaria especial —un store externo, un contexto, un gestor con su librería. Solid disuelve esa creencia mostrando que el componente nunca fue el dueño natural del estado, solo un lugar cómodo donde declararlo. Un signal es un valor con observadores; no le importa dónde se creó ni quién lo lee, y esa indiferencia es precisamente lo que lo hace tan portable. Subirlo al nivel de módulo no es un truco ni una optimización: es reconocer que la reactividad de grano fino ya era global en espíritu, porque la suscripción viaja con el acto de leer, no con la jerarquía de componentes. De ahí que en Solid el “gestor de estado” que otros ecosistemas convierten en librería con cientos de páginas de documentación se reduzca a un archivo que exporta un getter y unas funciones. La misma primitiva sirve para el contador de un componente y para la sesión de toda la aplicación; solo cambia dónde la colocas. Pero esta misma potencia trae su responsabilidad, y es la del ciclo de vida: un signal de módulo no muere nunca, y en el servidor eso significa que lo comparten todos los que atiende el proceso. La lección profunda es doble: primero, que no hace falta ceremonia para compartir estado, solo mover la declaración; y segundo, que “global” y “de larga vida” son la misma cosa, de modo que la pregunta correcta ante cada signal no es dónde lo declaro por comodidad sino cuánto debe vivir y quién debe verlo. Responder eso bien, signal a signal, es diseñar la arquitectura de estado de tu aplicación.
- Extrae un contador a un módulo que exporte
county una acciónincrementar; consúmelo desde dos componentes distintos y comprueba que comparten el mismo valor. - Guárdate el setter dentro del módulo y expón solo acciones con nombre; razona qué garantía de encapsulación ganas.
- Intenta crear un
createEffecta nivel de módulo y observa el aviso de la consola; arréglalo envolviéndolo encreateRoot. - Explica por qué un signal de módulo nunca ejecuta
onCleanupy qué implica eso para su ciclo de vida. - Argumenta por qué guardar el usuario autenticado en un signal de módulo es un bug en SSR y dónde debería vivir ese estado.