Actualizadores en la ruta: derivar del valor previo
Cuando el valor nuevo depende del anterior, el último argumento de setStore puede ser una función que recibe el valor actual en esa hoja y devuelve el siguiente. Es el equivalente del actualizador funcional del signal, pero situado en cualquier profundidad del árbol. Evita lecturas externas del proxy, elimina cierres obsoletos y hace atómica cada transición.
El último argumento como función
Ya sabes que setStore(...ruta, valor) navega hasta una hoja y escribe. La regla se completa así: si ese último argumento es una función, Solid la invoca con el valor actual de la hoja y usa lo que devuelve como valor nuevo. El caso canónico cabe en una línea:
import { createStore } from "solid-js/store";const [estado, setEstado] = createStore({ contador: 0, abierto: false });setEstado("contador", (c) => c + 1); // incrementa leyendo el previosetEstado("abierto", (o) => !o); // alterna la banderaEl actualizador (c) => c + 1 no lee estado.contador por su cuenta: lo recibe como argumento, ya resuelto y fresco, directamente de las manos del setter. La transición «lo que había, más uno» queda expresada como una sola operación indivisible, sin una variable temporal que envejezca entre la lectura y la escritura.
Compáralo con la alternativa ingenua, que parece equivalente y no lo es:
// Fragil: lee fuera, calcula, escribe. Tres tiempos, un valor que puede envejecersetEstado("contador", estado.contador + 1);Aquí estado.contador + 1 se evalúa antes de que el setter corra, capturando el valor en ese instante. Dentro de un lote, dentro de un efecto, o si dos de estas escrituras se encadenan, ese número puede haber quedado atrás. El actualizador no puede envejecer porque recibe el valor en el momento exacto de la escritura, no un instante antes.
batch(() => { setEstado("contador", (c) => c + 1); setEstado("contador", (c) => c + 1); // parte del +1 anterior: termina en +2});// Con estado.contador + 1 repetido, ambas verian el mismo valor: solo subiria +1Ese es el escenario donde la diferencia deja de ser teórica: dos incrementos encadenados que con lectura externa colapsan en uno, y con el actualizador se acumulan correctamente porque cada uno ve lo que dejó el anterior.
Actualizadores a cualquier profundidad
La potencia real llega al combinar la ruta con el actualizador: la función no vive solo en la raíz, sino en la hoja a la que llegues, por honda que esté. Los argumentos previos te llevan; el último deriva.
const [estado, setEstado] = createStore({ carrito: { total: 0, lineas: { impuesto: 0 } },});setEstado("carrito", "total", (t) => t + 9.99);setEstado("carrito", "lineas", "impuesto", (i) => +(i * 1.21).toFixed(2));La función recibe siempre el valor de esa hoja, no del estado entero: (t) => t + 9.99 ve el número que hay en carrito.total, aislado. El actualizador no conoce ni necesita conocer el resto del árbol; opera sobre su hoja como una función pura de un valor a otro, lo que lo hace trivial de extraer, nombrar y reutilizar.
Y hay un segundo argumento que pocos usan y conviene conocer: la ruta recorrida hasta la hoja. Es útil cuando un mismo callback se aplica en varias posiciones y necesita saber dónde está actuando.
// El actualizador recibe (valorActual, rutaRecorrida)const incrementar = (v: number, ruta: (string | number)[]) => { console.debug("escribiendo en", ruta.join(".")); return v + 1;};setEstado("carrito", "total", incrementar); // ruta: carrito.totalEsa segunda pieza convierte a un actualizador genérico en uno consciente de su contexto sin acoplarlo a una ruta fija: la lógica de «suma uno» vive en un sitio y se aplica allá donde la coloques, informándose de su posición solo si le importa.
Escribir sin suscribirse
Hay un beneficio silencioso en derivar dentro del setter que solo se revela dentro de un ámbito reactivo. Leer estado.contador en el cuerpo de un efecto o un memo suscribe ese ámbito a la hoja: desde ese instante, cada cambio del contador vuelve a ejecutarlo. El actualizador, en cambio, recibe el valor por argumento sin tocar el proxy, así que escribe sin crear dependencia alguna.
createEffect(() => { registrar(estado.evento); // esto SI suscribe: reaccionas a evento // Si leyeras estado.contador aqui para incrementarlo, el efecto // se suscribiria a si mismo: un bucle o re-ejecuciones de mas setEstado("contador", (c) => c + 1); // no lee, no se suscribe});El caso patológico es el bucle accidental: un efecto que lee un campo y lo escribe se convierte en su propia fuente y se re-dispara sin fin. Con el actualizador el peligro desaparece de raíz, porque escribir el contador ya no implica leerlo. Esta es la razón profunda por la que la comunidad de Solid prefiere (c) => c + 1 incluso fuera de efectos: no es solo elegancia, es que mantiene tu grafo de dependencias limpio de aristas que nunca quisiste dibujar.
Qué devuelve el actualizador
El valor que retorna la función entra en el store con exactamente las mismas reglas que un valor literal en esa posición, y esto encierra una sutileza que conviene tener clara. Si devuelves una primitiva o un array, reemplaza la hoja. Si devuelves un objeto y en esa posición ya había un objeto, Solid lo fusiona superficialmente.
// Devuelve objeto sobre objeto -> se FUSIONA, no reemplazasetEstado("usuario", (u) => ({ edad: u.edad + 1 }));// nombre y demas claves sobreviven aunque no las menciones// Devuelve array -> REEMPLAZA la referencia enterasetEstado("etiquetas", (e) => [...e, "nueva"]);De aquí sale una consecuencia que sorprende a quien viene de reducers inmutables: al devolver un objeto desde un actualizador de store, esparcir ...u para conservar las claves intactas es redundante, porque la fusión ya las conserva. (u) => ({ edad: u.edad + 1 }) basta. El spread no hace daño, pero delata que aún piensas en términos de reemplazo total cuando el store razona en parches. Los arrays, en cambio, no se fusionan: ahí el spread sí es necesario, porque devolver un array reemplaza la referencia entera y debes construir el contenido completo del nuevo.