setStore por ruta: de la raíz a la hoja
El setter de un store no reemplaza el estado: navega hasta la propiedad exacta que quieres tocar mediante una lista de argumentos que forman una ruta, y escribe solo esa hoja. Cuando el valor final es un objeto y ya había un objeto, Solid fusiona superficialmente en vez de sustituir. Dominar la ruta y la fusión es dominar la escritura granular sin spread manual.
Un signal se escribe entero: le entregas un valor nuevo y reemplaza al anterior. Un store, en cambio, se escribe por dentro. Su setter no espera el estado completo, sino una ruta —una sucesión de claves— que lo lleva hasta la propiedad concreta que quieres cambiar, y allí, y solo allí, escribe. Es la diferencia entre repintar el cuadro entero y retocar una pincelada. Entender cómo esa ruta se expresa como argumentos, y qué hace Solid cuando el destino es un objeto, es el cimiento de todo lo que sigue en este nivel.
- Leer
setStore(...ruta, valor)como una navegación hasta una hoja del árbol. - Distinguir cuándo el último argumento reemplaza y cuándo se fusiona.
- Comprender la fusión superficial de objetos y su límite de un solo nivel.
- Escribir campos profundos sin reconstruir a mano cada nivel con
spread.
La ruta como lista de argumentos
createStore devuelve una tupla familiar: el estado —un proxy de solo lectura— y un setter. Pero ese setter no funciona como el de un signal. Su firma es variádica: todos los argumentos menos el último forman la ruta hacia una propiedad, y el último es el valor que se escribe en ella.
import { createStore } from "solid-js/store";
const [estado, setEstado] = createStore({
usuario: { nombre: "Ada", edad: 30 },
tema: "oscuro",
});
setEstado("tema", "claro"); // estado.tema = "claro"
setEstado("usuario", "nombre", "Grace"); // estado.usuario.nombre = "Grace"
Lee setEstado("usuario", "nombre", "Grace") en voz alta: entra en usuario, entra en nombre, escribe la cadena. Cada string es un salto de nivel en el árbol; el último argumento es la carga. No hay spread, no hay reconstrucción de los niveles intermedios, no reemplazas usuario para cambiar su nombre. Y lo decisivo: solo se despierta quien lee estado.usuario.nombre. Los lectores de estado.usuario.edad o de estado.tema no reciben notificación alguna, porque el store instala un signal perezoso por propiedad y tú acabas de tocar exactamente uno.
Esa ruta es una dirección, no una copia del camino. El signal grueso te obligaba a reconstruir cada nivel del trayecto con spread sobre spread; el store te deja nombrar el destino y viaja solo. La verbosidad que en un signal crecía con la profundidad, aquí es constante: un argumento por nivel, ni uno más.
La lectura, en cambio, no pasa por el setter: accedes al proxy como a cualquier objeto. La asimetría es deliberada y hay que grabarla desde el principio.
const nombre = estado.usuario.nombre; // leer: acceso directo al proxy
setEstado("usuario", "nombre", "Grace"); // escribir: siempre por el setter
Leer estado.usuario.nombre dentro de un ámbito reactivo suscribe a esa hoja; escribir exige el canal del setter. Nunca al revés: el proxy es de solo lectura y asignarle directamente es un error que verás con detalle en la última lección de este nivel.
Fusión superficial: el objeto como parche
¿Y si el último argumento no es una hoja sino un objeto? Aquí aparece la regla que define el carácter del store: cuando el valor final es un objeto y en esa posición ya había otro objeto, Solid no reemplaza, fusiona. Copia clave por clave del nivel superior del objeto que le pasas y deja intacto todo lo que no mencionas.
// usuario era { nombre: "Ada", edad: 30 }
setEstado("usuario", { edad: 31 });
// ahora es { nombre: "Ada", edad: 31 } -> nombre se conservo
No tuviste que esparcir ...usuario para preservar nombre: la fusión lo hace por ti. El objeto que entregas se comporta como un parche, un conjunto de claves que se aplican sobre lo existente. Es el mismo principio en la raíz, donde el parche cae sobre el estado completo:
// Fusion en la raiz: parchea varias claves de primer nivel a la vez
setEstado({ tema: "claro", cargando: false });
// usuario y cualquier otra clave del estado permanecen intactos
Piensa siempre en términos de parche, no de reemplazo: al pasar un objeto no dices «el estado ahora es esto», dices «aplica estas claves sobre lo que ya hay». Ese cambio de mentalidad —de sustituir a parchear— es la mitad de lo que distingue escribir un store de escribir un signal, y explica por qué casi nunca necesitas spread para conservar lo que no tocas.
flowchart TD
A[setStore usuario edad 31] --> B{cuantos argumentos}
B --> C[usuario forma la ruta]
C --> D[el ultimo argumento es el valor]
D --> E{que tipo es el valor}
E -->|primitiva o array| F[reemplaza esa propiedad]
E -->|objeto sobre objeto| G[fusiona clave por clave]
style F fill:#89b4fa,color:#11111b
style G fill:#a6e3a1,color:#11111bReemplazar la hoja
El valor final es una primitiva, un array o una referencia opaca. Solid sobrescribe esa propiedad y notifica solo a sus lectores.
Fusionar el objeto
El valor final es un objeto y ya había un objeto. Solid aplica cada clave del nivel superior como un parche y conserva lo que no nombras.
El límite del parche: la fusión no es profunda
La fusión es una espada de doble filo, y confundir su alcance es la fuente del primer bug serio con stores. Es superficial: fusiona solo el primer nivel del objeto que pasas. Cualquier objeto anidado dentro de ese parche reemplaza por completo a su homólogo, no se funde con él.
const [estado, setEstado] = createStore({
editor: { fuente: "mono", tema: { fondo: "negro", texto: "blanco" } },
});
// PELIGRO: tema anidado reemplaza entero, texto desaparece
setEstado("editor", { tema: { fondo: "gris" } });
// editor.tema es ahora { fondo: "gris" } -> texto se perdio
// BIEN: alarga la ruta hasta la hoja profunda
setEstado("editor", "tema", "fondo", "gris"); // texto intacto
La regla operativa es nítida: la fusión te ahorra el spread de un nivel, no de todos. Si el campo que cambias vive a tres niveles de profundidad, extiende la ruta tres niveles y escribe la hoja; no entregues un objeto anidado esperando que Solid lo funda hacia dentro, porque no lo hará. La ruta larga es granular y segura; el objeto anidado es un reemplazo silencioso que borra hermanos.
Ten a mano un modelo mental para no vacilar en caliente: la fusión cubre un nivel de anidamiento, la ruta cubre tantos como argumentos le des. Cuando dudes de si un cambio profundo se fundirá o reemplazará, no adivines, alarga la ruta hasta la hoja y la ambigüedad se disuelve por completo.
// Ante la duda, ruta larga hasta la hoja: granular y sin borrar hermanos
setEstado("editor", "tema", "texto", "gris"); // fondo sobrevive
Dentro de una fusión, una clave con valor undefined no se salta: escribe undefined sobre esa propiedad, que es precisamente la forma de eliminarla del store. Si tu intención era dejar el campo intacto, no lo incluyas en el parche. Incluirlo con undefined es una orden explícita de borrado, no un no-op. Este matiz muerde cuando construyes el parche dinámicamente y una clave llega undefined sin que lo pretendieras.
El último argumento puede ser una función en vez de un valor literal: setStore("usuario", (u) => ({ edad: u.edad + 1 })). Solid la llama con el valor actual y funde el objeto que devuelve, con la misma regla superficial. Es el puente hacia la siguiente lección: cuando el valor nuevo depende del anterior, no lo leas por fuera, derívalo dentro del propio setter.
Varias claves hermanas en una escritura
Un segmento de la ruta no tiene que ser una sola clave: puede ser un array de claves, y entonces Solid aplica el resto de la ruta a cada una. Sobre un objeto, eso significa escribir varias propiedades hermanas de una sola vez, con el mismo valor o el mismo actualizador.
const [caja, setCaja] = createStore({ ancho: 100, alto: 100, margen: 8 });
setCaja(["ancho", "alto"], 0); // ambas a 0 en una escritura
setCaja(["ancho", "alto", "margen"], 0); // restablece las tres de golpe
setCaja(["ancho", "alto"], (v) => v * 2); // duplica las dos hermanas
No es azúcar cosmético: cada clave listada se escribe como su propia hoja y notifica a sus propios lectores, pero la operación entera se agrupa en un único lote, así que la propagación ocurre una vez. Es el gesto natural para restablecer un grupo de campos afines o aplicarles una misma transformación sin repetir el nombre del store por cada uno. En la lección 4 verás que ese mismo array de claves, cuando el contenedor es un array, se convierte en una lista de índices; la mecánica es idéntica, cambia solo la naturaleza de las claves.
Lo que de verdad cambia al pasar del signal al store no es la sintaxis, es el modelo mental de qué significa escribir. En un signal, escribir es sustituir: hay un único valor y lo reemplazas entero, de modo que toda la reactividad es atómica y toda edición profunda te obliga a reconstruir el camino con spread para no romper el contrato de identidad. En un store, escribir es direccionar: el estado es un árbol de propiedades, cada una con su propio signal perezoso, y el setter recibe una dirección —la ruta— y una carga —el valor— igual que una escritura en memoria recibe un puntero y un dato. Por eso los argumentos no son azúcar sintáctico: son las coordenadas del punto exacto del árbol donde vas a escribir, y esa precisión es lo que permite que solo despierte quien observa esa coordenada, ni un lector más. La fusión superficial es la otra cara de la misma moneda: cuando la carga es un objeto, Solid no interpreta que quieres sustituir el nodo entero —eso destruiría hermanos que nadie te pidió tocar— sino que quieres aplicar un parche, un delta de claves sobre lo que ya vive ahí. Que sea superficial no es una limitación caprichosa sino una consecuencia de la coherencia: fundir en profundidad exigiría recorrer y decidir en cada nivel, y el store prefiere que tú declares la profundidad extendiendo la ruta, porque nadie conoce mejor que tú hasta dónde llega el cambio. Interiorizado esto, las dos preguntas que gobiernan cada escritura se vuelven triviales: ¿hasta qué hoja llega mi cambio? —esa es la longitud de la ruta— y ¿sustituyo o parcheo? —eso lo decide si la carga es una primitiva o un objeto—. Respondidas, el spread en escalera desaparece de tu código, no porque lo hayas evitado con astucia, sino porque el store lo volvió innecesario.
- Crea un store con
usuarioanidado y cambia solousuario.edadcon una ruta de tres argumentos; confirma queusuario.nombreconserva su valor. - Fusiona un parche
{ edad: 31 }sobreusuarioy verifica que preserva las demás claves sin escribirspread. - Provoca el bug: pasa un objeto anidado como parche y comprueba que el hijo profundo reemplaza a su homólogo y borra hermanos.
- Corrige ese mismo cambio alargando la ruta hasta la hoja profunda; razona por qué ahora nada se pierde.
- Añade un efecto que lea
usuario.nombrey otro que leausuario.edad; cambia solo la edad y cuenta cuál de los dos se dispara.