Saltar al contenido

Store vs muchos signals: elegir por la forma del estado

Muchos signals sueltos son ideales para un puñado de átomos independientes que reemplazas enteros. Un store escala mejor cuando el estado tiene forma: anidado, cohesivo, con claves dinámicas, que viaja por contexto como una unidad o que se hidrata desde el servidor. La decisión no es de gusto sino de topología: cuenta cuántas piezas hay, cómo se relacionan y cómo viajan.

Dos formas de estado: átomos sueltos y árboles con forma

Los signals brillan cuando el estado son átomos independientes: valores que no comparten estructura, que cambian por separado y que reemplazas enteros. Un contador, una bandera de menú abierto, unas coordenadas. Cada uno es su propio par, y esa dispersión es una virtud mientras sigan siendo pocos y de verdad independientes.

TSX
// Muchos signals: átomos independientes, cada uno con su parconst [x, setX] = createSignal(0);const [y, setY] = createSignal(0);const [abierto, setAbierto] = createSignal(false);

El store, en cambio, brilla cuando el estado tiene forma: es un objeto anidado cuyas partes se relacionan, se leen juntas, se pasan juntas y evolucionan como una unidad conceptual. Un documento, un perfil, un carrito, el estado de un formulario grande. No son N valores sueltos que casualmente conviven; son un dato con estructura interna.

TSX
// Store: una forma cohesiva, un solo setter que escribe por rutaconst [doc, setDoc] = createStore({  titulo: "",  meta: { autor: "", tags: [] as string[] },  bloques: [] as Bloque[],});

Qué gana el store cuando el estado tiene forma

El primer beneficio es la ergonomía de la escritura anidada: setDoc("meta", "autor", "Ada") frente a reconstruir con spread cada nivel de un signal. Pero eso es lo menos interesante. Lo que de verdad hace escalar al store son tres propiedades que los signals sueltos no ofrecen sin dolor.

La primera es que viaja como una unidad. Pasar el estado a un componente hijo o a través de createContext es pasar un solo par [estado, setEstado]. Con signals sueltos tendrías que agrupar a mano N pares en un objeto contenedor, y ese objeto es, de hecho, un store peor hecho.

TSX
// El store viaja por contexto como un único valor tipadoconst Ctx = createContext<[typeof doc, typeof setDoc]>();// Con diez signals sueltos tendrías que empaquetar diez pares a mano

La segunda es que admite claves dinámicas con naturalidad. Cuando no conoces los campos de antemano —un índice de registros por id, un mapa de estados por clave— el store te deja escribir por ruta con una clave calculada. Reproducir eso con signals exige un mapa de signals y gestionar a mano su creación y destrucción.

TSX
// Claves dinámicas: un índice de registros por idconst [porId, setPorId] = createStore<Record<string, Registro>>({});setPorId(id, "leido", true); // ruta con clave calculada en runtime

El equivalente con signals delata la fricción: necesitas un mapa que guarde un par por clave y gestionar a mano su creación y su limpieza, porque no puedes declarar un signal para una clave que aún no conoces. Lo que en el store es una escritura por ruta, con signals es una estructura de datos auxiliar que mantener viva.

TSX
// Con signals: un mapa de pares gestionado manualmenteconst registros = new Map<string, ReturnType<typeof createSignal<Registro>>>();function marcarLeido(id: string) {  const par = registros.get(id);  if (!par) return;                    // cada entrada la creas y la limpias tú  par[1]((r) => ({ ...r, leido: true }));}

La tercera es que se reconcilia y se serializa como el objeto que es. Hidratar desde el servidor con reconcile fusiona conservando identidad; volcar el estado con unwrap te da el objeto plano listo para JSON.stringify. Un manojo de signals no tiene una forma serializable natural: la tienes que ensamblar tú, campo a campo, cada vez.

TSX
import { unwrap, reconcile } from "solid-js/store";// Serializar: el store es un objeto, unwrap te da el planolocalStorage.setItem("doc", JSON.stringify(unwrap(doc)));// Hidratar desde el servidor conservando la identidad de las filassetDoc("bloques", reconcile(bloquesDelServidor));

Cuándo bastan (y cuándo sobran) los signals

El error simétrico es real y frecuente: acorralar átomos sin relación dentro de un mismo store solo para tener menos variables sueltas. Un store cuyas propiedades no comparten estructura ni se leen juntas no es estado con forma; es un cajón desastre con coste de proxy y sin ninguna cohesión que lo justifique. Tres booleanos de UI sin relación entre sí son tres signals, no un store de tres campos.

TSX
// Antipatrón: átomos sin relación amontonados por tener menos variablesconst [ui, setUi] = createStore({ menuAbierto: false, cargando: false, oscuro: true });// Mejor: cada átomo independiente es su propio signalconst [menuAbierto, setMenuAbierto] = createSignal(false);const [cargando, setCargando] = createSignal(false);const [oscuro, setOscuro] = createSignal(true);

El coste de este antipatrón no es solo de rendimiento —que también, por el proxy que no aprovechas—, sino sobre todo de diseño: agrupar cosas no relacionadas sugiere una cohesión que no existe y confunde a quien lea el código, que buscará en vano la estructura que justifique el store. La agrupación debe comunicar relación; agrupar por conveniencia miente sobre la forma del estado.

La combinación de ambos también es legítima y a menudo la respuesta correcta. Una colección grande vive en un store para editarse por elemento, mientras un par de estados de UI atómicos —el elemento seleccionado, un modo de vista, una bandera de carga— viven en signals sueltos a su lado. No estás obligado a elegir un único recipiente para toda la aplicación: eliges por cada pieza según su forma. Un id seleccionado es un átomo aunque apunte a un objeto del store, porque lo que reemplazas entero es el id.

Un último matiz de método sobre la escala: la fricción de los signals sueltos no crece de forma lineal, sino combinatoria. Dos signals no molestan; diez que en realidad son un mismo dato con forma te obligan a pasarlos juntos, a mantenerlos coherentes entre sí y a recordar cuáles cambian con cuáles. Ese acoplamiento implícito es precisamente lo que un store hace explícito y gestiona por ti. Cuando notes que un grupo de signals siempre viaja junto, ya tienes tu respuesta: no eran átomos independientes, eran las hojas de un árbol que aún no habías nombrado.