wandres.dev
CREATESTORE · reactividad anidada

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.

⏱ 15 min

Un signal y un store no compiten por ser mejor: resuelven formas distintas de estado. La pregunta de un ingeniero maduro no es cuál prefiere, sino cuál encaja con la topología de este estado concreto. Un puñado de valores atómicos e independientes vive de maravilla en signals sueltos; un árbol cohesivo que crece, viaja por la aplicación y se hidrata desde la red pide a gritos un store. Confundir los casos produce dos síntomas opuestos: signals dispersos que deberían ser un objeto, y stores triviales que envuelven un booleano en maquinaria de proxy inútil.

🎯 Al terminar esta lección sabrás
  • Distinguir estado atómico e independiente de estado con forma y cohesión.
  • Enumerar qué gana un store cuando el estado es anidado, crece o tiene claves dinámicas.
  • Ver por qué un store viaja mejor por contexto que un manojo de signals.
  • Reconocer el antipatrón de meter átomos sin relación en un store solo por tener menos variables.

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.

// Muchos signals: átomos independientes, cada uno con su par
const [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.

// Store: una forma cohesiva, un solo setter que escribe por ruta
const [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.

// El store viaja por contexto como un único valor tipado
const 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.

// Claves dinámicas: un índice de registros por id
const [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.

// Con signals: un mapa de pares gestionado manualmente
const 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.

import { unwrap, reconcile } from "solid-js/store";

// Serializar: el store es un objeto, unwrap te da el plano
localStorage.setItem("doc", JSON.stringify(unwrap(doc)));

// Hidratar desde el servidor conservando la identidad de las filas
setDoc("bloques", reconcile(bloquesDelServidor));
💡
El estado que crece pide store desde el principio

Una heurística práctica: si sospechas que el estado va a crecer —más campos, más anidamiento, colecciones que se llenan— empieza con un store aunque hoy quepa en dos signals. Migrar tres signals a un store a mitad de proyecto obliga a reescribir cada lectura y cada escritura repartidas por la base de código; empezar con un store cuesta lo mismo hoy y no te cobra esa deuda mañana. La forma del estado tiende a estabilizarse pronto: escúchala.

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.

// Antipatrón: átomos sin relación amontonados por tener menos variables
const [ui, setUi] = createStore({ menuAbierto: false, cargando: false, oscuro: true });

// Mejor: cada átomo independiente es su propio signal
const [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.

flowchart TD
Q[como es tu estado] --> A[pocos atomos independientes]
Q --> B[forma cohesiva anidada o que crece]
A --> SIG[muchos signals sueltos]
B --> ST[un store]
B --> C[claves dinamicas o viaja por contexto]
C --> ST
style SIG fill:#89b4fa,color:#11111b
style ST fill:#a6e3a1,color:#11111b

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.

⚛️

Muchos signals

Pocos átomos independientes que reemplazas enteros. Máxima simplicidad mientras sigan siendo pocos, sin estructura compartida y sin viajar juntos por la aplicación.

🌳

Un store

Estado con forma: anidado, cohesivo, con claves dinámicas, que viaja por contexto como una unidad y se hidrata o serializa como el objeto que es.

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.

La forma del estado decide el recipiente, no tu preferencia

La madurez con estado en Solid consiste en dejar de preguntar qué primitiva prefiero y empezar a leer la topología de cada pieza antes de elegir. Hazte tres preguntas concretas y la respuesta se cae sola. Primera: cuántas piezas son y si están relacionadas —si son pocos átomos que cambian por separado y no comparten estructura, son signals; si son partes de un mismo dato con forma interna, es un store. Segunda: cómo viaja el estado —si lo pasas de un lado a otro como una unidad, por contexto o por props, un store es un solo valor cohesivo mientras que N signals son N cosas que empaquetar y desempaquetar a mano, reinventando peor lo que el store ya te da. Tercera: cómo cambia y de dónde viene —si tiene claves que no conoces de antemano, si crece, si se hidrata desde el servidor o se serializa, el store lo modela nativamente con rutas dinámicas, reconcile y unwrap, mientras que los signals te obligan a construir esa maquinaria a mano. Y no olvides el reverso: un store trivial sobre un booleano paga el coste del proxy sin usar su granularidad, y amontonar átomos sin relación en un store por ahorrarte variables no crea cohesión, solo la simula. La regla que zanja casi todos los casos cabe en una frase: átomos independientes al signal, datos con forma al store, y no fuerces ninguno de los dos contra la naturaleza de lo que estás modelando.

⚔️ Clasifica tu estado por su forma
  1. Modela un formulario de ocho campos primero con ocho signals y luego con un store; compara cuánto código cuesta pasar todo el estado a un componente hijo.
  2. Implementa un índice por id con claves dinámicas usando un store; intenta lo mismo con signals y anota la fricción de gestionar su ciclo de vida.
  3. Toma tres piezas de estado real de tu aplicación y clasifícalas como átomo suelto o forma cohesiva, justificando cada una.
  4. Mete a propósito tres booleanos sin relación en un mismo store y argumenta por escrito por qué es un antipatrón.
  5. Escribe, en una sola frase, la regla que usarás para decidir entre store y signals.