El patrón factory createXStore()
En vez de esparcir signals y acciones sueltas por un módulo, una función factory createXStore() encapsula el estado y su lógica dentro y devuelve una API limpia y tipada. Ese pliegue separa la definición del store de su instanciación: el mismo código puede producir un singleton de cliente o una instancia por petición, y TypeScript deriva el tipo de la API sin que lo escribas dos veces.
De variables sueltas a una fábrica
Compara los dos estilos. En el primero, el estado global es un puñado de exportaciones que comparten un módulo por casualidad de vecindad. En el segundo, hay una función que produce un objeto cohesionado, y todo lo privado —los signals, el setter— queda cerrado dentro del closure.
// store/contador.tsimport { createRoot, createSignal, createMemo } from "solid-js";export function createContadorStore() { return createRoot((dispose) => { const [cuenta, setCuenta] = createSignal(0); const doble = createMemo(() => cuenta() * 2); return { get cuenta() { return cuenta(); }, // getter: lectura reactiva get doble() { return doble(); }, incrementar: () => setCuenta((n) => n + 1), reiniciar: () => setCuenta(0), dispose, }; });}Todo lo mutable —setCuenta— vive en el closure y nunca escapa. La factory devuelve una API cuya forma tú decides: cuenta y doble como getters de solo lectura, incrementar y reiniciar como verbos, y dispose como interruptor. Quien la consume ve un objeto limpio, no un revoltijo de tuplas de signals. La reactividad viaja intacta porque los getters llaman a los signals al leerse: acceder a store.cuenta dentro de un efecto o del JSX suscribe a ese signal, exactamente como si lo llamaras a mano.
Una API limpia y tipada
El segundo regalo del patrón es el tipado, y es casi gratis. En lugar de declarar a mano una interfaz que describa la API —y mantenerla sincronizada con la implementación—, dejas que TypeScript derive el tipo del valor de retorno de la factory con ReturnType.
export type ContadorStore = ReturnType<typeof createContadorStore>;Ahora ContadorStore es exactamente la forma que devuelve la factory: si añades una acción o cambias un getter, el tipo se actualiza solo, sin que puedan divergir la interfaz declarada y la real. Ese tipo derivado es lo que anotarás en un contexto, en la firma de una función que reciba el store, o en el valor de un createContext.
import { createContext } from "solid-js";import { createContadorStore, type ContadorStore } from "./store/contador";// El contexto habla en terminos del tipo derivado, nunca desactualizado.export const ContadorCtx = createContext<ContadorStore>();Merece subrayar lo que se gana con ReturnType: la API y su tipo no pueden desincronizarse, porque son la misma fuente. En un diseño con la interfaz escrita a mano es habitual añadir un método a la implementación y olvidar el tipo, o al revés, y el compilador calla porque cada uno es válido por separado. Con el tipo derivado ese desfase es imposible por construcción: si el método existe en el retorno, existe en el tipo; si no, tampoco. Es la clase de garantía barata —una sola línea— que elimina de raíz toda una familia de bugs de mantenimiento.
Diseñar bien esa superficie es la parte que merece cuidado. Una buena API de store se parece a la de un objeto bien pensado: lecturas explícitas, un conjunto cerrado de operaciones con nombre, cero fugas de la maquinaria interna.
Definir no es instanciar
El giro conceptual del patrón está en el nombre: createContadorStore es una fábrica, no una instancia. Definir el store —escribir la factory— y crearlo —llamarla— son dos actos distintos, y esa separación es lo que le da al patrón su alcance. El mismo archivo que define la factory no decide cuántas veces se llama ni quién la posee; esa decisión la toma el consumidor.
De ahí salen dos modos de uso, y elegir bien entre ellos es medio nivel resumido en una decisión. El primero: exportar una única instancia creada al importar, y tienes de vuelta el singleton de módulo, ahora encapsulado y tipado.
// Singleton: una instancia para toda la app cliente.export const contador = createContadorStore();El segundo: exportar la factory misma y dejar que cada consumidor cree su instancia, típicamente para proveerla por contexto a un subárbol. Cada llamada produce un store independiente, con su propio estado y su propio dispose.
// Una instancia por subarbol, provista por contexto.function ProveedorContador(props: { children: any }) { const store = createContadorStore(); onCleanup(store.dispose); // muere con el proveedor return ( <ContadorCtx.Provider value={store}> {props.children} </ContadorCtx.Provider> );}La misma factory sirve a ambos mundos sin cambiar una línea de su cuerpo. Esa neutralidad respecto a la instanciación es lo que la hace tan útil, y lo que la conecta con las dos lecciones que cierran el nivel: cuándo un singleton basta y cuándo el estado quiere vivir por subárbol o por petición.
Hay un tercer beneficio que la práctica valora tanto como la encapsulación y el tipado: la testabilidad. Una factory es una función que fabrica un store fresco cada vez que la llamas, así que cada test puede crear su propia instancia aislada, ejercitar sus acciones y disponerla al terminar, sin variables de módulo que arrastren estado de un caso al siguiente. El singleton importado obliga a resetear estado global entre tests; la factory elimina esa fricción porque no hubo estado global que resetear hasta que tú decidiste crearlo. Definir sin instanciar es, también, definir sin contaminar.