wandres.dev
CREATEMUTABLE · el store mutable

createMutable: el proxy de escritura directa

createMutable devuelve un único objeto proxy que puedes mutar en sitio: asignar a state.x = 1 es, por sí mismo, una operación reactiva, sin setter ni tupla. Cómo comparte el motor de rastreo por propiedad de createStore pero invierte su contrato —de escritura canalizada a escritura libre—, qué envuelve en profundidad, y por qué esa comodidad aparente reorganiza quién es dueño del cambio.

⏱ 15 min

Todo lo que has aprendido del store descansa en un contrato: el estado se lee del proxy y se escribe por un canal aparte, el setter. createMutable rompe ese contrato a propósito. Te devuelve un único objeto —sin tupla, sin setter— cuyas propiedades puedes reasignar a mano, y hace que la asignación misma, state.x = 1, sea la operación reactiva. Por debajo late el mismo motor de rastreo por propiedad de createStore; lo que cambia es quién sostiene el bolígrafo de la escritura, y con él, toda la disciplina que gobierna el flujo de datos.

🎯 Al terminar esta lección sabrás
  • Entender qué devuelve createMutable: un único proxy escribible, no una tupla.
  • Ver por qué asignar a state.x = 1 es en sí mismo un acto reactivo.
  • Contrastar con createStore: quién posee la escritura y por dónde pasa.
  • Reconocer el rastreo por propiedad y el envoltorio profundo que ambos comparten.

Un único proxy, sin setter

createStore te entrega dos cosas: un proxy de solo lectura y una función setStore que es la única puerta legítima para escribir. createMutable colapsa esas dos cosas en una: un proxy que es a la vez la ventana de lectura y la superficie de escritura. Su firma lo delata —createMutable<T>(state: T): T—: entra un objeto, sale un objeto del mismo tipo, y no hay segundo elemento en el retorno porque no hay canal separado.

import { createMutable } from "solid-js/store";

const state = createMutable({ contador: 0, nombre: "Ada" });

// leer: como cualquier propiedad
state.contador; // 0

// escribir: asignacion directa, sin setter
state.contador = 1;
state.nombre = "Grace";

No hay setContador, no hay ruta con comillas, no hay actualizador funcional. Escribes como escribirías sobre un objeto plano de JavaScript. La diferencia invisible es que ese objeto no es plano: es un proxy que intercepta cada get para registrar dependencias y cada set para notificarlas. Y ahí está el corazón del primitivo: la asignación misma es el disparador.

Aquí está el detalle que lo cambia todo. En un objeto normal, obj.x = 1 es una operación muda: cambia un valor y nadie se entera. En un createMutable, esa misma línea atraviesa el set del proxy, que compara el valor nuevo con el viejo y, si difiere, notifica a todos los que hayan leído state.x dentro de un ámbito de rastreo. La asignación es el disparador.

import { createEffect } from "solid-js";

const state = createMutable({ contador: 0 });

createEffect(() => {
  console.log("contador:", state.contador); // se suscribe al leer
});

state.contador = 1; // el efecto vuelve a correr: imprime 1
state.contador = 2; // corre otra vez: imprime 2

El pacto de rastreo es idéntico al de todo Solid —leer dentro de un ámbito reactivo suscribe— pero la escritura ha cambiado de forma. Ya no invocas una función que declara tu intención de cambio; ejecutas una mutación y el proxy la traduce en notificación por ti. Leer sigue siendo leer; escribir ha dejado de ser una llamada para volverse un efecto secundario de la asignación.

flowchart LR
A[state.x = 1] --> P[proxy intercepta el set]
P --> C{el valor difiere}
C -->|si| N[notifica a los lectores de state.x]
C -->|no| Z[no hace nada]
N --> E[efectos y memos dependientes recalculan]
style A fill:#89b4fa,color:#11111b
style N fill:#a6e3a1,color:#11111b
style Z fill:#45475a,color:#cdd6f4

Mismo motor, contrato invertido

createMutable y createStore no son parientes lejanos: son la misma máquina de rastreo por propiedad con la superficie de escritura invertida. En el store, mutar el proxy directamente es un error —en desarrollo, Solid te avisa— porque toda escritura debe pasar por setStore para ser rastreada. En el mutable, esa prohibición se levanta: el proxy es escribible, y la ruta canalizada desaparece.

// createStore: escritura canalizada por setStore
import { createStore } from "solid-js/store";
const [store, setStore] = createStore({ contador: 0 });
setStore("contador", 1);          // unica via legitima
// store.contador = 1;            // en dev, Solid advierte: no rastreado

// createMutable: escritura libre sobre el proxy
import { createMutable } from "solid-js/store";
const mut = createMutable({ contador: 0 });
mut.contador = 1;                 // esta ES la via

La consecuencia va más allá de la sintaxis. En el store, el conjunto de lugares donde el estado puede cambiar es finito y localizable: busca setStore y tendrás el censo completo de las escrituras. En el mutable, cualquier fragmento de código que tenga una referencia al objeto puede reasignar cualquier propiedad, en cualquier momento. La escritura deja de ser un evento con nombre propio para disolverse en el flujo imperativo del programa. Es la misma diferencia que separa un formulario con un botón de “guardar” de un documento que se autoguarda con cada tecla: ambos persisten, pero uno tiene puntos de control y el otro no.

ℹ️
Getters y anidamiento vienen incluidos

El proxy es profundo: los objetos y arrays anidados se envuelven de forma perezosa la primera vez que los tocas, así que state.usuario.direccion.calle = "Nueva" es reactivo hasta la hoja. Y como cualquier store, respeta los getters que definas en el objeto inicial —get doble() { return this.n * 2 }— convirtiéndolos en derivaciones reactivas. Esa afinidad con las clases y los getters es justo la potencia que explora la lección siguiente.

Reactividad de grano fino, no de objeto

Un malentendido frecuente es creer que mutar el proxy notifica a “quien lea el estado”. No: notifica a quien lea la propiedad concreta que tocaste. El rastreo es por clave, igual que en el store. Un efecto que solo lee state.nombre no se despierta cuando cambias state.contador, aunque ambos vivan en el mismo objeto.

const state = createMutable({ contador: 0, nombre: "Ada" });

createEffect(() => console.log(state.nombre));   // solo depende de nombre

state.contador = 99; // el efecto NO corre: no lee contador
state.nombre = "Grace"; // el efecto corre: lee nombre

Esta granularidad es la que hace atractivo al primitivo: obtienes la ergonomía de mutar un objeto plano y, aun así, cada campo se rastrea por separado sin que reconstruyas nada. Es también la que lo hace peligroso, porque la reactividad se vuelve invisible en el punto de escritura —un tema para la lección 3—. Por ahora, fija el modelo mental exacto: createMutable no es “un store más fácil”, es el mismo store con la puerta de escritura abierta de par en par.

Desestructurar y unwrap: dos formas de perder el proxy

La reactividad vive en el acto de leer una propiedad a través del proxy. Dos operaciones cotidianas rompen ese acto y capturan valores muertos. La primera es desestructurar: const { contador } = state lee state.contador una única vez, guarda el número y corta todo vínculo; a partir de ahí contador es una copia congelada que nunca se refresca. Es la misma regla que gobierna las props, y el mismo error de principiante.

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

const state = createMutable({ contador: 0, config: { zoom: 1 } });

// MAL: captura el valor una vez, sin reactividad
const { contador } = state;

// BIEN: lee la propiedad en el punto de uso, dentro del ambito reactivo
createEffect(() => console.log(state.contador));

// unwrap devuelve el objeto crudo, fuera del proxy: util para interop,
// peligroso para escribir, porque mutar lo desenvuelto NO notifica
const crudo = unwrap(state);
crudo.contador = 99; // cambia el dato, pero nadie se entera

unwrap es la operación inversa a createMutable: te devuelve el objeto subyacente, sin proxy, para entregárselo a código que no debe rastrear o para comparar estructuras. Pero recuerda que fuera del proxy no hay reactividad: leer del objeto crudo no suscribe y escribir en él no notifica. Trátalo como una ventana de solo lectura hacia el dato, no como un atajo de escritura.

La comodidad no es gratis: mueve el coste de la sintaxis a la trazabilidad

Es tentador leer createMutable como la versión sin fricción de createStore: menos ceremonia, menos rutas entre comillas, menos actualizadores funcionales, la misma reactividad. Y en la superficie es cierto. Pero la ceremonia que createStore te impone no es burocracia gratuita: es lo que convierte cada escritura en un evento localizable. Cuando toda mutación pasa por setStore, el conjunto de puntos donde el estado cambia es un objeto que puedes enumerar, buscar y auditar; el flujo de datos tiene una forma que puedes dibujar. createMutable te devuelve esos minutos de tecleo a cambio de disolver esa forma. Ya no hay un censo de escrituras porque cualquier línea con una referencia al proxy es una escritura en potencia; el grafo de “quién cambia qué” deja de ser una propiedad del código y pasa a existir solo en tu cabeza, reconstruido a mano cada vez que depuras. No estás eliminando un coste, lo estás trasladando: de la sintaxis, que la máquina verifica, a la trazabilidad, que solo tú sostienes. Por eso el equipo de Solid marca este primitivo como una herramienta de nicho y no como el camino por defecto. No porque su motor sea peor —es idéntico— sino porque el contrato que impone al programador es más débil, y los contratos débiles se cobran su factura tarde, en forma de mutaciones que nadie recuerda haber escrito. Entender esto desde la primera línea es lo que separa usar createMutable con criterio de usarlo por pereza.

⚔️ Palpa la escritura directa
  1. Crea un createMutable con { contador: 0, nombre: "Ada" } y un efecto que lea solo nombre; muta contador y confirma que el efecto no corre.
  2. Muta nombre y verifica que ahora sí corre: has palpado el rastreo por propiedad.
  3. Reescribe el mismo estado con createStore y comprueba que mutar el proxy directamente dispara el aviso de desarrollo.
  4. Añade una propiedad anidada direccion: { calle: "" } y muta state.direccion.calle; confirma que la reactividad llega hasta la hoja.
  5. Explica con tus palabras por qué “el mismo motor con el contrato invertido” describe mejor a createMutable que “un store más fácil”.