produce: mutar un borrador con reactividad intacta
produce entrega un borrador proxy sobre el que escribes con sintaxis de mutación directa, estilo Immer, mientras Solid registra cada cambio y lo traduce en actualizaciones granulares. No copia el estado ni rompe la inmutabilidad observable: sustituye la escalera de rutas por asignaciones legibles cuando el cambio toca varios campos, empuja a arrays o depende de condiciones.
Escribir por rutas es preciso, pero cuando un solo cambio toca cinco campos, empuja a un array y decide según una condición, la sucesión de setStore(...) se vuelve un poema repetitivo donde el nombre del store aparece una y otra vez y la lógica se disuelve entre paréntesis. produce es la respuesta de Solid a esa fricción: te da un borrador —un proxy vivo— sobre el que escribes como si mutaras un objeto normal, s.a.b = 1, s.lista.push(x), y por debajo traduce cada una de esas asignaciones en una actualización granular exacta. Ergonomía de mutación, corrección de inmutabilidad, sin renunciar a ninguna.
- Usar
producepara mutar un borrador proxy dentro desetStore. - Entender que el borrador registra cada escritura y notifica de forma granular.
- Distinguir
producede Immer: aquí no hay copia, hay rastreo directo. - Elegir entre ruta y
producesegún cuántos campos toca el cambio.
El borrador que registra sus mutaciones
produce es un modificador: una función que recibe otra función y devuelve algo que pasas a setStore en lugar de un valor. Dentro, recibes un borrador del estado y escribes sobre él con la sintaxis más natural del mundo —asignación, push, delete— sin devolver nada.
import { createStore, produce } from "solid-js/store";
const [estado, setEstado] = createStore({
usuario: { nombre: "Ada", edad: 30 },
etiquetas: ["a", "b"],
});
setEstado(
produce((s) => {
s.usuario.edad += 1;
s.usuario.nombre = "Grace";
s.etiquetas.push("c");
})
);
Ese borrador s no es el estado real ni una copia suya: es un proxy de escritura que intercepta cada asignación y cada método mutador y los convierte, uno a uno, en las mismas actualizaciones granulares que habrías provocado con rutas. Cuando la función termina, Solid ha registrado tres cambios independientes —edad, nombre, un elemento nuevo en etiquetas— y notifica a cada conjunto de lectores por separado. El código parece una mutación imperativa; el efecto es una reactividad de grano fino impecable.
Dos reglas enmarcan su uso. La primera: no devuelvas nada desde el borrador. produce observa mutaciones, no valores de retorno; devolver un objeto no reemplaza el estado, simplemente se ignora. La segunda: el borrador no debe escapar de la función. Guardarlo en una variable externa y mutarlo después es un error, porque su magia solo vive durante la ejecución del callback.
setEstado(produce((s) => {
delete s.usuario.edad; // borrar una clave: registrado
s.etiquetas.splice(0, 1); // mutar el array en sitio: registrado
return { otro: "estado" }; // IGNORADO: produce no mira el retorno
}));
El delete y el splice de arriba serían pecados capitales sobre el proxy de un createStore —que es de solo lectura— y sin embargo aquí son legítimos, porque el borrador los intercepta y traduce. Esa es la inversión clave: produce convierte las operaciones prohibidas fuera en el idioma natural dentro.
Puedes acotar produce a una rama
produce no está obligado a operar sobre la raíz. Combinado con una ruta, recibe como borrador solo el subárbol al que llegaste, lo que reduce el ruido y expresa mejor la intención cuando todo el cambio vive bajo una misma rama.
// produce sobre todo el estado
setEstado(produce((s) => { s.usuario.edad += 1; }));
// produce acotado a la rama usuario: el borrador ES usuario
setEstado("usuario", produce((u) => {
u.edad += 1;
u.nombre = "Grace";
}));
En la segunda forma, u es directamente el objeto usuario, no el estado entero. La ruta hace de prefijo y produce de bisturí local. Es la traducción natural de «todos estos cambios pertenecen al mismo sitio»: nombras el sitio una vez con la ruta y mutas dentro con libertad.
flowchart TD A[setStore produce fn] --> B[Solid crea un borrador proxy] B --> C[tu fn muta el borrador] C --> D[cada asignacion se registra] C --> E[cada push o delete se registra] D --> F[se traduce a actualizacion granular] E --> F F --> G[notifica solo a los lectores tocados] style B fill:#cba6f7,color:#11111b style F fill:#89b4fa,color:#11111b style G fill:#a6e3a1,color:#11111b
El proxy de produce intercepta el repertorio completo de la mutación imperativa: asignación de propiedades, delete, y los métodos mutadores de array —push, pop, shift, unshift, splice, sort, reverse, fill—. Justo los que en un signal eran una trampa, aquí son ciudadanos de primera, porque el borrador los traduce a actualizaciones granulares. Un límite conviene recordarlo: produce opera sobre nodos envolvibles —objetos y arrays—; no puedes usarlo para escribir un valor primitivo suelto en la raíz, para eso está la ruta directa.
No es Immer: no hay copia, hay rastreo
La sintaxis es idéntica a la de Immer y la comparación es inevitable, pero el motor es distinto y entender la diferencia te evita malos modelos mentales. Immer, en el mundo inmutable de React, toma tu estado, crea un borrador con copy-on-write y, al terminar, produce un objeto nuevo por compartición estructural que reemplaza al anterior; todo el propósito es fabricar una referencia fresca. produce de Solid no fabrica nada: escribe directamente sobre el store a través de su proxy, registrando cada mutación como una notificación granular, sin clonar ni devolver un estado nuevo.
Immer
Copy-on-write sobre un borrador, devuelve un estado inmutable nuevo. Su meta es una referencia fresca para que React detecte el cambio.
produce de Solid
Muta un proxy que rastrea cada escritura y la aplica al store in situ. Su meta es traducir mutaciones a notificaciones de grano fino.
La consecuencia práctica es doble. No pagas el coste de copiar un árbol en cada escritura, porque no se copia nada. Y no obtienes un valor de retorno que reemplace el estado, porque el estado se edita en su sitio, propiedad a propiedad. Immer resuelve «cómo genero barato un objeto inmutable nuevo»; produce resuelve «cómo escribo con comodidad de mutación sin perder la granularidad». Misma piel, distinto esqueleto.
Este contraste tiene un corolario que importa a la hora de razonar el rendimiento. En el mundo de Immer, cada produce deja tras de sí una nueva raíz inmutable y descarta la anterior, con el trabajo de compartición estructural que eso conlleva; el modelo es «historia de versiones», ideal para deshacer y rehacer, costoso si abusas. En Solid no hay versiones que fabricar ni descartar: el borrador anota deltas sobre el árbol vivo y desaparece. Por eso produce es barato de repetir a alta frecuencia —arrastrar un slider, teclear en un campo— sin generar presión de memoria, algo que un produce de Immer sí acumularía si no lo gestionas.
produce brilla mutando lo que existe, pero no es la herramienta para sustituir un subárbol por una referencia completamente nueva de forma óptima, ni para volcar datos del servidor conservando identidad de filas: eso es trabajo de reconcile, que verás en la lección 5. Si dentro de produce reasignas s.lista = otraLista entera, funciona, pero pierdes la ventaja del rastreo fino y remontas a los lectores de esa rama. Usa produce para editar; usa reconcile para fusionar lo externo.
Ruta o produce: la frontera de la legibilidad
Ambas escrituras producen exactamente la misma reactividad granular, así que la elección es de claridad, no de rendimiento. Un cambio que toca una hoja se expresa mejor como ruta: setStore("a", "b", v) es directo y sin ceremonia. Un cambio que toca varias hojas relacionadas, empuja a arrays o ramifica según condiciones se expresa mejor con produce, donde la lógica respira como código imperativo normal.
// Una hoja: la ruta gana en concision
setEstado("usuario", "edad", 31);
// Varias hojas con logica: produce gana en legibilidad
setEstado(produce((s) => {
s.usuario.edad += 1;
if (s.usuario.edad >= 18) s.usuario.rol = "adulto";
s.historial.push({ campo: "edad", en: Date.now() });
}));
Fuerza la decisión con una pregunta: ¿este cambio cabe cómodo en una sola ruta? Si sí, ruta. Si necesitas encadenar tres o más setStore, o si aparece un if, un bucle o un push, produce casi siempre resulta más honesto sobre lo que hace. No es purismo: rutas para lo puntual, borrador para lo compuesto.
Un tercer criterio, más sutil, inclina la balanza hacia produce cuando un campo que escribes depende de otro que también cambias en el mismo gesto. Dentro del borrador lees el valor ya actualizado, sin coordinar el orden de varias rutas a mano:
setEstado(produce((s) => {
s.subtotal = calcular(s.lineas);
s.impuesto = s.subtotal * 0.21; // ve el subtotal recien fijado
s.total = s.subtotal + s.impuesto;
}));
Con rutas sueltas tendrías que releer el store entre escritura y escritura, reintroduciendo el problema de lecturas intermedias que el store bien usado evita. El borrador, al ser un único ámbito coherente, vuelve natural la dependencia entre campos en lugar de frágil.
Toda la tensión de la gestión de estado reactivo se resume en un dilema aparente: mutar es cómodo pero invisible para un sistema que detecta cambios por identidad de referencia; ser inmutable es observable pero incómodo, porque te obliga a reconstruir el camino con spread o a encadenar rutas. produce disuelve el dilema mostrando que era falso. Lo que hace no es permitirte mutar el estado —eso seguiría siendo un bug, porque mutar a espaldas del grafo rompe el contrato de identidad que hace observable el cambio— sino darte un borrador que parece mutable y por debajo es un registro de intenciones: cada s.a.b = 1 no toca el estado real directamente, sino que se anota como «la hoja a.b pasa a valer 1», y al cerrar la función Solid aplica esas anotaciones como las mismas actualizaciones granulares que habrías declarado con rutas. La sintaxis imperativa es una fachada honesta sobre una escritura declarativa: escribes en el idioma con el que tu cerebro piensa el cambio —asigna, empuja, borra— y el sistema lo traduce al idioma en el que necesita registrarlo —notifica a estos lectores, deja quietos a aquellos—. Por eso la comparación con Immer, siendo iluminadora en la superficie, engaña en el fondo: Immer necesita fabricar un objeto nuevo porque su mundo detecta cambios comparando referencias de raíz, mientras que Solid ya rastrea por propiedad y no necesita ninguna referencia nueva, solo saber qué hojas cambiaron. produce no es, pues, un clon de Immer trasplantado, sino su inverso conceptual: donde Immer construye inmutabilidad a partir de mutación aparente, Solid registra granularidad a partir de mutación aparente. Y esa es la lección que trasciende la herramienta: en un sistema de grano fino, la comodidad de mutar y la corrección de observar no son enemigas que haya que sacrificar una por otra, sino dos caras que un buen proxy hace coincidir. Elegir produce sobre una escalera de rutas no es rebajar el rigor; es reconocer que la legibilidad de un cambio compuesto también es una forma de corrección, la que evita el bug que nace de no entender el propio código.
- Con un solo
produce, modifica tres campos de distintas ramas del store y confirma con efectos que cada rama notifica por separado. - Empuja un elemento a un array dentro de
producey verifica que la lista reacciona sin reconstruirla conspread. - Acota un
producea una subrama con una ruta previa y comprueba que el borrador es solo ese subárbol. - Intenta devolver un objeto desde el borrador y confirma que el retorno se ignora: el estado solo cambia por las mutaciones.
- Reescribe una cadena de cuatro
setStorepor rutas como un únicoproducey decide, por legibilidad, cuál prefieres y por qué.