wandres.dev
ESTADO GLOBAL · stores compartidos

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.

⏱ 15 min

En otros frameworks el estado global es una conquista: necesitas una librería, un proveedor en la raíz, un hook para consumir. En Solid es el punto de partida. Declaras un createSignal en la cima de un módulo y ya tienes un singleton reactivo que cualquier componente puede importar y compartir, sin ceremonia y sin árbol. La reactividad de grano fino no vive dentro de los componentes: vive en un grafo propio que un módulo puede sostener igual de bien. Lo difícil, por eso, no es crear el estado global —eso es una línea— sino diseñar su superficie: qué se lee, qué se muta y por dónde.

🎯 Al terminar esta lección sabrás
  • Declarar un signal o un store en el ámbito de módulo como singleton reactivo compartido.
  • Entender por qué la reactividad de grano fino es global sin necesitar un proveedor.
  • Exportar getters de solo lectura y acciones con nombre, ocultando el setter.
  • Reconocer el límite del patrón: signals y stores sí, memos y efectos todavía no.

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.ts
import { createSignal } from "solid-js";

const [cuenta, setCuenta] = createSignal(0);

export { cuenta };                                   // getter de solo lectura
export 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.

flowchart TD
M[modulo contador.ts] --> S[createSignal cuenta]
S --> A[Marcador lee cuenta]
S --> B[Boton llama incrementar]
A --> V[misma fuente reactiva compartida]
B --> V
style M fill:#a6e3a1,color:#11111b
style S fill:#89b4fa,color:#11111b
style V fill:#f9e2af,color:#11111b

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.ts
import { 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 fuera
export 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.

Exporta: getters de lectura

El signal como función de solo lectura, o el proxy de un store, inmutable desde fuera. Leer es libre; cada consumidor decide qué mira.

Exporta: acciones con nombre

Cada mutación válida como un verbo —entrar, salir, alternarTema—: el conjunto cerrado de transiciones que el estado admite.

🚫

Oculta: el setter crudo

setCuenta o setEstado nunca salen del módulo. Son la única capacidad de escritura, y quedan tras las acciones.

🚫

Oculta: la tupla completa

Exportar el par valor y setter entero entrega la escritura a todo el mundo y disuelve el único punto de mutación.

💡
Los verbos son la documentación del store

Un store cuya superficie pública es sesion, entrar, salir y alternarTema se explica solo: la lista de acciones es el catálogo de todo lo que puede pasarle al estado. Cuando en cambio exportas el setter crudo, ese catálogo desaparece y cualquier archivo puede inventar una mutación nueva. Nombrar las acciones no es cosmética: es convertir el conjunto de transiciones válidas en algo cerrado, legible y buscable con un simple «buscar usos».

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 DESARROLLO
import { createSignal, createMemo } from "solid-js";

const [precio, setPrecio] = createSignal(100);

// createMemo en el modulo: sin owner, nunca se dispone -> advertencia
const 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.

ℹ️
Qué cubre bien un store de módulo de solo datos

No subestimes cuánto abarca el patrón sin cómputos. El tema activo, las preferencias del usuario, los ítems de un carrito, un catálogo cargado una vez, banderas de características, si un panel lateral está abierto: todo eso son datos que se leen y se mutan por acciones, sin necesidad de un solo memo ni efecto. La derivación —un total, un recuento, un filtrado— casi siempre puede vivir en el punto de consumo, dentro del componente que ya tiene su owner, en lugar de en el módulo. Solo cuando esa derivación debe cachearse de forma global, o disparar un efecto compartido, el módulo pelado se queda corto. Hasta ahí, datos y acciones bastan, y bastan para mucho.

Lo global nunca fue el problema difícil

Vienes, casi con seguridad, de un mundo donde el estado global era una hazaña de ingeniería: un store externo, un proveedor que envuelve la aplicación, hooks que se suscriben, comparaciones para evitar renders de más. Toda esa maquinaria existe por una razón concreta y ajena a Solid: en un modelo basado en re-render, la reactividad está atada al árbol de componentes, y sacarla de ahí —hacerla verdaderamente global— exige reconstruir a mano un sistema de suscripción que el framework no te da de fábrica. En Solid ese trabajo ya está hecho, y hecho en la capa correcta. La reactividad no es una propiedad de los componentes: es un grafo autónomo de signals, memos y efectos que existe con o sin árbol. Un componente es solo un cliente de ese grafo. Por eso un signal en un módulo es tan reactivo como uno dentro de un componente —no hay dos categorías, hay una sola primitiva colocada en distintos sitios—. La consecuencia mental es profunda: la ausencia de ceremonia que sientes al escribir tu primer store de módulo no es que Solid te esté dejando hacer algo peligroso o incompleto; es que el problema que las librerías de estado resuelven, aquí, simplemente no existe. Lo global era difícil por una limitación del modelo, no por la naturaleza del estado. Cuando la reactividad se libera del árbol, compartir estado entre componentes deja de ser arquitectura y vuelve a ser lo que siempre debió ser: una variable con alcance de módulo. Toda la disciplina que queda —getters, acciones, ocultar el setter— no es para domar un poder peligroso, sino la misma higiene de siempre: decidir con cuidado qué se lee y qué se muta.

⚔️ Diseña la superficie de un store de módulo
  1. Crea store/contador.ts con un signal privado; exporta solo el getter cuenta y las acciones incrementar y reiniciar. Consúmelo desde dos componentes hermanos y confirma que comparten valor.
  2. Convierte el estado a createStore con los campos usuario y tema; exporta el proxy de lectura y las acciones, y verifica desde fuera que el proxy no puede mutarse sin el setter.
  3. Comprueba la reactividad de grano fino: un componente que solo lee sesion.tema no debe re-evaluarse al cambiar usuario.
  4. Añade en la cima del módulo un createMemo y observa la advertencia de desarrollo; explica en una frase por qué un signal no la provoca y un memo sí.
  5. Argumenta por qué exportar setEstado en vez de acciones con nombre degrada la mantenibilidad, aunque el código funcione igual.