Un store a nivel de módulo
El estado global más simple de Solid no es una librería ni un proveedor: es un signal o un store declarado en la cima de un archivo. Como la reactividad de grano fino vive fuera del árbol de componentes, ese valor de módulo ya es un singleton reactivo que todos comparten. La disciplina no está en crearlo, sino en qué exportas: getters de solo lectura y acciones con nombre, nunca el setter crudo.
El módulo es el contenedor
Un módulo de ES se evalúa una sola vez por programa: la primera importación ejecuta su cuerpo, y todas las demás reciben las mismas exportaciones. Si en ese cuerpo creas un signal, ese signal existe una vez y se comparte por definición. No hace falta nada más para tener estado global: el sistema de módulos ya te da la unicidad, y Solid le presta la reactividad.
// store/contador.tsimport { createSignal } from "solid-js";const [cuenta, setCuenta] = createSignal(0);export { cuenta }; // getter de solo lecturaexport const incrementar = () => setCuenta((n) => n + 1);export const reiniciar = () => setCuenta(0);Cualquier componente que importe cuenta lee la misma fuente reactiva. Cuando uno llama a incrementar, todos los que dependen de cuenta() se actualizan —solo ellos, solo los nodos del DOM que la usan— sin que ninguno conozca a los otros ni exista un proveedor que los conecte.
import { cuenta, incrementar } from "../store/contador";function Marcador() { return <p>Valor: {cuenta()}</p>;}function Boton() { return <button onClick={incrementar}>Sumar</button>;}Marcador y Boton pueden vivir en ramas del árbol que no se tocan, montarse y desmontarse de forma independiente, y aun así comparten estado. El pegamento no es la jerarquía de componentes: es el módulo.
Esta unicidad no es una promesa de Solid, sino del propio sistema de módulos, y por eso es tan sólida: da igual cuántos archivos importen contador.ts, todos reciben el mismo signal porque el cuerpo del módulo se evaluó una sola vez. Solid no añade nada a esa garantía; solo le presta reactividad al valor que el módulo ya hacía único. La consecuencia práctica es liberadora: no hay ningún registro de estado global que mantener ni raíz que arrancar. El estado global deja de ser un subsistema que montas y vuelve a ser una variable que declaras.
Exportar getters y acciones, no el setter
El error de principiante es exportar el par entero: export const [cuenta, setCuenta] = .... Eso convierte cada consumidor en un mutador potencial, y el estado deja de tener un único punto de cambio. La disciplina que hace mantenible un store de módulo es simétrica a la de un objeto bien encapsulado: expón lecturas y operaciones con nombre, esconde la escritura cruda.
El getter cuenta es de solo lectura por construcción —es una función que devuelve el valor, no puede escribirlo—. Las acciones incrementar y reiniciar son el único camino a setCuenta, que nunca sale del módulo. Así toda mutación pasa por una operación nombrada, con semántica clara, donde puedes validar invariantes, registrar cambios o coordinar varios signals a la vez.
Con createStore la encapsulación es aún más natural, porque el proxy que devuelve ya es de solo lectura desde fuera: no puedes mutarlo sin la función de escritura. Basta con exportar el proxy y las acciones, y retener el setter.
// store/sesion.tsimport { createStore } from "solid-js/store";type Estado = { usuario: string | null; tema: "claro" | "oscuro" };const [estado, setEstado] = createStore<Estado>({ usuario: null, tema: "claro" });export const sesion = estado; // proxy de lectura, mutar es imposible desde fueraexport const entrar = (nombre: string) => setEstado("usuario", nombre);export const alternarTema = () => setEstado("tema", (t) => (t === "claro" ? "oscuro" : "claro"));Quien importe sesion puede leer sesion.usuario y sesion.tema con reactividad de grano fino —un componente que solo lee sesion.tema no se re-evalúa cuando cambia usuario—, pero no tiene forma de escribir. La capacidad de mutar es setEstado, y esa se queda dentro. La API pública es un objeto de lectura y un puñado de verbos.
Signals sí, computaciones todavía no
Hay un límite preciso en lo que puedes poner con seguridad en la cima de un módulo, y conviene conocerlo desde el primer día. Un signal y un store son contenedores de datos: no observan a nadie, no tienen ciclo de vida propio, no necesitan un dueño que los limpie. Por eso viven felices en el ámbito de módulo para siempre.
Un createMemo o un createEffect, en cambio, son computaciones: nodos del grafo que rastrean dependencias y deben poder destruirse. Si los creas en la cima de un módulo, Solid te avisa en desarrollo de que estás creando una computación fuera de una raíz o un render, y de que nunca se dispondrá. El efecto correrá, sí, pero queda huérfano: sin owner que lo limpie, es una fuga esperando a ocurrir.
// store/derivado.ts — ESTO AVISA EN DESARROLLOimport { createSignal, createMemo } from "solid-js";const [precio, setPrecio] = createSignal(100);// createMemo en el modulo: sin owner, nunca se dispone -> advertenciaconst conIva = createMemo(() => precio() * 1.21);La regla práctica de esta lección es entonces limpia: en un store de módulo, datos sí, cómputos no. Signals y stores puros cubren la enorme mayoría de los casos de estado global compartido. En el momento en que tu estado global necesite una derivación memoizada o un efecto —sincronizar con localStorage, registrar cambios, derivar un total cacheado—, ya no basta el módulo pelado: necesitas darle una raíz. Ese es exactamente el tema de la siguiente lección, createRoot.