Saltar al contenido

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.

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.

TSX
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.

TSX
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.

TSX
// produce sobre todo el estadosetEstado(produce((s) => { s.usuario.edad += 1; }));// produce acotado a la rama usuario: el borrador ES usuariosetEstado("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.

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.

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.

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.

TSX
// Una hoja: la ruta gana en concisionsetEstado("usuario", "edad", 31);// Varias hojas con logica: produce gana en legibilidadsetEstado(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:

TSX
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.